• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2#
3# Copyright 2018 - The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16r"""LocalImageLocalInstance class.
17
18Create class that is responsible for creating a local instance AVD with a
19local image. For launching multiple local instances under the same user,
20The cuttlefish tool requires 3 variables:
21- ANDROID_HOST_OUT: To locate the launch_cvd tool.
22- HOME: To specify the temporary folder of launch_cvd.
23- CUTTLEFISH_INSTANCE: To specify the instance id.
24Acloud user must either set ANDROID_HOST_OUT or run acloud with --local-tool.
25The user can optionally specify the folder by --local-instance-dir and the
26instance id by --local-instance.
27
28The adb port and vnc port of local instance will be decided according to
29instance id. The rule of adb port will be '6520 + [instance id] - 1' and the
30vnc port will be '6444 + [instance id] - 1'.
31e.g:
32If instance id = 3 the adb port will be 6522 and vnc port will be 6446.
33
34To delete the local instance, we will call stop_cvd with the environment
35variable [CUTTLEFISH_CONFIG_FILE] which is pointing to the runtime cuttlefish
36json.
37
38To run this program outside of a build environment, the following setup is
39required.
40- One of the local tool directories is a decompressed cvd host package,
41  i.e., cvd-host_package.tar.gz.
42- If the instance doesn't require mixed images, the local image directory
43  should be an unzipped update package, i.e., <target>-img-<build>.zip,
44  which contains a super image.
45- If the instance requires mixing system image, the local image directory
46  should be an unzipped target files package, i.e.,
47  <target>-target_files-<build>.zip,
48  which contains misc info and images not packed into a super image.
49- If the instance requires mixing system image, one of the local tool
50  directories should be an unzipped OTA tools package, i.e., otatools.zip.
51"""
52
53import collections
54import logging
55import os
56import re
57import shutil
58import subprocess
59import sys
60
61from acloud import errors
62from acloud.create import base_avd_create
63from acloud.create import create_common
64from acloud.internal import constants
65from acloud.internal.lib import cvd_utils
66from acloud.internal.lib import ota_tools
67from acloud.internal.lib import utils
68from acloud.internal.lib.adb_tools import AdbTools
69from acloud.list import list as list_instance
70from acloud.list import instance
71from acloud.public import report
72from acloud.setup import mkcert
73
74
75logger = logging.getLogger(__name__)
76
77_SUPER_IMAGE_NAME = "super.img"
78_MIXED_SUPER_IMAGE_NAME = "mixed_super.img"
79_CMD_CVD_START = " start"
80_CMD_CVD_VERSION = " version"
81_CMD_LAUNCH_CVD_ARGS = (
82    " -daemon -config=%s -system_image_dir %s -instance_dir %s "
83    "-undefok=report_anonymous_usage_stats,config,proxy_fastboot "
84    "-report_anonymous_usage_stats=y")
85_CMD_LAUNCH_CVD_HW_ARGS = " -cpus %s -x_res %s -y_res %s -dpi %s -memory_mb %s"
86_CMD_LAUNCH_CVD_DISK_ARGS = (
87    " -blank_data_image_mb %s -data_policy always_create")
88_CMD_LAUNCH_CVD_WEBRTC_ARGS = " -start_webrtc=true"
89_CMD_LAUNCH_CVD_VNC_ARG = " -start_vnc_server=true"
90_CMD_LAUNCH_CVD_SUPER_IMAGE_ARG = " -super_image=%s"
91_CMD_LAUNCH_CVD_BOOT_IMAGE_ARG = " -boot_image=%s"
92_CMD_LAUNCH_CVD_VENDOR_BOOT_IMAGE_ARG = " -vendor_boot_image=%s"
93_CMD_LAUNCH_CVD_KERNEL_IMAGE_ARG = " -kernel_path=%s"
94_CMD_LAUNCH_CVD_INITRAMFS_IMAGE_ARG = " -initramfs_path=%s"
95_CMD_LAUNCH_CVD_VBMETA_IMAGE_ARG = " -vbmeta_image=%s"
96_CMD_LAUNCH_CVD_NO_ADB_ARG = " -run_adb_connector=false"
97# Supported since U.
98_CMD_LAUNCH_CVD_NO_FASTBOOT_ARG = " -proxy_fastboot=false"
99_CMD_LAUNCH_CVD_INSTANCE_NUMS_ARG = " -instance_nums=%s"
100# Connect the OpenWrt device via console file.
101_CMD_LAUNCH_CVD_CONSOLE_ARG = " -console=true"
102_CMD_LAUNCH_CVD_WEBRTC_DEIVE_ID = " -webrtc_device_id=%s"
103_CONFIG_RE = re.compile(r"^config=(?P<config>.+)")
104_CONSOLE_NAME = "console"
105# Files to store the output when launching cvds.
106_STDOUT = "stdout"
107_STDERR = "stderr"
108_MAX_REPORTED_ERROR_LINES = 10
109
110# In accordance with the number of network interfaces in
111# /etc/init.d/cuttlefish-common
112_MAX_INSTANCE_ID = 10
113
114# TODO(b/213521240): To check why the delete function is not work and
115# has to manually delete temp folder.
116_INSTANCES_IN_USE_MSG = ("All instances are in use. Try resetting an instance "
117                         "by specifying --local-instance and an id between 1 "
118                         "and %d. Alternatively, to run 'acloud delete --all' "
119                         % _MAX_INSTANCE_ID)
120_CONFIRM_RELAUNCH = ("\nCuttlefish AVD[id:%d] is already running. \n"
121                     "Enter 'y' to terminate current instance and launch a "
122                     "new instance, enter anything else to exit out[y/N]: ")
123
124# The first two fields of this named tuple are image folder and CVD host
125# package folder which are essential for local instances. The following fields
126# are optional. They are set when the AVD spec requires to mix images.
127ArtifactPaths = collections.namedtuple(
128    "ArtifactPaths",
129    ["image_dir", "host_bins", "host_artifacts", "misc_info", "ota_tools_dir",
130     "system_image", "boot_image", "vendor_boot_image", "kernel_image",
131     "initramfs_image", "vendor_image", "vendor_dlkm_image", "odm_image",
132     "odm_dlkm_image"])
133
134
135class LocalImageLocalInstance(base_avd_create.BaseAVDCreate):
136    """Create class for a local image local instance AVD."""
137
138    @utils.TimeExecute(function_description="Total time: ",
139                       print_before_call=False, print_status=False)
140    def _CreateAVD(self, avd_spec, no_prompts):
141        """Create the AVD.
142
143        Args:
144            avd_spec: AVDSpec object that tells us what we're going to create.
145            no_prompts: Boolean, True to skip all prompts.
146
147        Returns:
148            A Report instance.
149        """
150        # Running instances on local is not supported on all OS.
151        result_report = report.Report(command="create")
152        if not utils.IsSupportedPlatform(print_warning=True):
153            result_report.UpdateFailure(
154                "The platform doesn't support to run acloud.")
155            return result_report
156        if not utils.IsSupportedKvm():
157            result_report.UpdateFailure(
158                "The environment doesn't support virtualization.")
159            return result_report
160
161        artifact_paths = self.GetImageArtifactsPath(avd_spec)
162
163        try:
164            ins_ids, ins_locks = self._SelectAndLockInstances(avd_spec)
165        except errors.CreateError as e:
166            result_report.UpdateFailure(str(e))
167            return result_report
168
169        try:
170            for ins_id, ins_lock in zip(ins_ids, ins_locks):
171                if not self._CheckRunningCvd(ins_id, no_prompts):
172                    # Mark as in-use so that it won't be auto-selected again.
173                    ins_lock.SetInUse(True)
174                    sys.exit(constants.EXIT_BY_USER)
175
176            result_report = self._CreateInstance(ins_ids, artifact_paths,
177                                                 avd_spec, no_prompts)
178            # Set the state to in-use if the instances start successfully.
179            # Failing instances are not set to in-use so that the user can
180            # restart them with the same IDs.
181            if result_report.status == report.Status.SUCCESS:
182                for ins_lock in ins_locks:
183                    ins_lock.SetInUse(True)
184            return result_report
185        finally:
186            for ins_lock in ins_locks:
187                ins_lock.Unlock()
188
189    def _SelectAndLockInstances(self, avd_spec):
190        """Select the ids and lock these instances.
191
192        Args:
193            avd_spec: AVCSpec for the device.
194
195        Returns:
196            The instance ids and the LocalInstanceLock that are locked.
197        """
198        main_id, main_lock = self._SelectAndLockInstance(avd_spec)
199        ins_ids = [main_id]
200        ins_locks = [main_lock]
201        for _ in range(2, avd_spec.num_avds_per_instance + 1):
202            ins_id, ins_lock = self._SelectOneFreeInstance()
203            ins_ids.append(ins_id)
204            ins_locks.append(ins_lock)
205        logger.info("Selected instance ids: %s", ins_ids)
206        return ins_ids, ins_locks
207
208    def _SelectAndLockInstance(self, avd_spec):
209        """Select an id and lock the instance.
210
211        Args:
212            avd_spec: AVDSpec for the device.
213
214        Returns:
215            The instance id and the LocalInstanceLock that is locked by this
216            process.
217
218        Raises:
219            errors.CreateError if fails to select or lock the instance.
220        """
221        if avd_spec.local_instance_id:
222            ins_id = avd_spec.local_instance_id
223            ins_lock = instance.GetLocalInstanceLock(ins_id)
224            if ins_lock.Lock():
225                return ins_id, ins_lock
226            raise errors.CreateError("Instance %d is locked by another "
227                                     "process." % ins_id)
228        return self._SelectOneFreeInstance()
229
230    @staticmethod
231    def _SelectOneFreeInstance():
232        """Select one free id and lock the instance.
233
234        Returns:
235            The instance id and the LocalInstanceLock that is locked by this
236            process.
237
238        Raises:
239            errors.CreateError if fails to select or lock the instance.
240        """
241        for ins_id in range(1, _MAX_INSTANCE_ID + 1):
242            ins_lock = instance.GetLocalInstanceLock(ins_id)
243            if ins_lock.LockIfNotInUse(timeout_secs=0):
244                return ins_id, ins_lock
245        raise errors.CreateError(_INSTANCES_IN_USE_MSG)
246
247    # pylint: disable=too-many-locals,too-many-statements
248    def _CreateInstance(self, instance_ids, artifact_paths, avd_spec,
249                        no_prompts):
250        """Create a CVD instance.
251
252        Args:
253            instance_ids: List of integer of instance ids.
254            artifact_paths: ArtifactPaths object.
255            avd_spec: AVDSpec for the instance.
256            no_prompts: Boolean, True to skip all prompts.
257
258        Returns:
259            A Report instance.
260        """
261        local_instance_id = instance_ids[0]
262        webrtc_port = self.GetWebrtcSigServerPort(local_instance_id)
263        if avd_spec.connect_webrtc:
264            utils.ReleasePort(webrtc_port)
265
266        cvd_home_dir = instance.GetLocalInstanceHomeDir(local_instance_id)
267        create_common.PrepareLocalInstanceDir(cvd_home_dir, avd_spec)
268        super_image_path = None
269        vbmeta_image_path = None
270        if artifact_paths.system_image or artifact_paths.vendor_image:
271            super_image_path = os.path.join(cvd_home_dir,
272                                            _MIXED_SUPER_IMAGE_NAME)
273            ota = ota_tools.OtaTools(artifact_paths.ota_tools_dir)
274            ota.MixSuperImage(
275                super_image_path, artifact_paths.misc_info,
276                artifact_paths.image_dir,
277                system_image=artifact_paths.system_image,
278                vendor_image=artifact_paths.vendor_image,
279                vendor_dlkm_image=artifact_paths.vendor_dlkm_image,
280                odm_image=artifact_paths.odm_image,
281                odm_dlkm_image=artifact_paths.odm_dlkm_image)
282            if artifact_paths.vendor_image:
283                vbmeta_image_path = os.path.join(cvd_home_dir,
284                                                 "disabled_vbmeta.img")
285                ota.MakeDisabledVbmetaImage(vbmeta_image_path)
286        runtime_dir = instance.GetLocalInstanceRuntimeDir(local_instance_id)
287        # TODO(b/168171781): cvd_status of list/delete via the symbolic.
288        self.PrepareLocalCvdToolsLink(cvd_home_dir, artifact_paths.host_bins)
289        if avd_spec.mkcert and avd_spec.connect_webrtc:
290            self._TrustCertificatesForWebRTC(artifact_paths.host_artifacts)
291        if not avd_spec.use_launch_cvd:
292            self._LogCvdVersion(artifact_paths.host_bins)
293
294        hw_property = None
295        if avd_spec.hw_customize:
296            hw_property = avd_spec.hw_property
297        config = self._GetConfigFromAndroidInfo(
298            os.path.join(artifact_paths.image_dir,
299                         constants.ANDROID_INFO_FILE))
300        cmd = self.PrepareLaunchCVDCmd(hw_property,
301                                       avd_spec.connect_adb,
302                                       avd_spec.connect_fastboot,
303                                       artifact_paths,
304                                       runtime_dir,
305                                       avd_spec.connect_webrtc,
306                                       avd_spec.connect_vnc,
307                                       super_image_path,
308                                       avd_spec.launch_args,
309                                       config or avd_spec.flavor,
310                                       avd_spec.openwrt,
311                                       avd_spec.use_launch_cvd,
312                                       instance_ids,
313                                       avd_spec.webrtc_device_id,
314                                       vbmeta_image_path)
315
316        result_report = report.Report(command="create")
317        instance_name = instance.GetLocalInstanceName(local_instance_id)
318        try:
319            self._LaunchCvd(cmd, local_instance_id, artifact_paths.host_bins,
320                            artifact_paths.host_artifacts,
321                            cvd_home_dir, (avd_spec.boot_timeout_secs or
322                                           constants.DEFAULT_CF_BOOT_TIMEOUT))
323            logs = cvd_utils.FindLocalLogs(runtime_dir, local_instance_id)
324        except errors.LaunchCVDFail as launch_error:
325            logs = cvd_utils.FindLocalLogs(runtime_dir, local_instance_id)
326            err_msg = ("Cannot create cuttlefish instance: %s\n"
327                       "For more detail: %s/launcher.log" %
328                       (launch_error, runtime_dir))
329            if constants.ERROR_MSG_WEBRTC_NOT_SUPPORT in str(launch_error):
330                err_msg = (
331                    "WEBRTC is not supported in current build. Please try VNC "
332                    "such as '$acloud create --autoconnect vnc'")
333            result_report.SetStatus(report.Status.BOOT_FAIL)
334            result_report.SetErrorType(constants.ACLOUD_BOOT_UP_ERROR)
335            result_report.AddDeviceBootFailure(
336                instance_name, constants.LOCALHOST, None, None, error=err_msg,
337                logs=logs)
338            return result_report
339
340        active_ins = list_instance.GetActiveCVD(local_instance_id)
341        if active_ins:
342            update_data = None
343            if avd_spec.openwrt:
344                console_dir = os.path.dirname(
345                    instance.GetLocalInstanceConfig(local_instance_id))
346                console_path = os.path.join(console_dir, _CONSOLE_NAME)
347                update_data = {"screen_command": f"screen {console_path}"}
348            result_report.SetStatus(report.Status.SUCCESS)
349            result_report.AddDevice(instance_name, constants.LOCALHOST,
350                                    active_ins.adb_port, active_ins.vnc_port,
351                                    webrtc_port, logs=logs,
352                                    update_data=update_data)
353            # Launch vnc client if we're auto-connecting.
354            if avd_spec.connect_vnc:
355                utils.LaunchVNCFromReport(result_report, avd_spec, no_prompts)
356            if avd_spec.connect_webrtc:
357                utils.LaunchBrowserFromReport(result_report)
358            if avd_spec.unlock_screen:
359                AdbTools(active_ins.adb_port).AutoUnlockScreen()
360        else:
361            err_msg = "cvd_status return non-zero after launch_cvd"
362            logger.error(err_msg)
363            result_report.SetStatus(report.Status.BOOT_FAIL)
364            result_report.SetErrorType(constants.ACLOUD_BOOT_UP_ERROR)
365            result_report.AddDeviceBootFailure(
366                instance_name, constants.LOCALHOST, None, None, error=err_msg,
367                logs=logs)
368        return result_report
369
370    @staticmethod
371    def GetWebrtcSigServerPort(instance_id):
372        """Get the port of the signaling server.
373
374        Args:
375            instance_id: Integer of instance id.
376
377        Returns:
378            Integer of signaling server port.
379        """
380        return constants.WEBRTC_LOCAL_PORT + instance_id - 1
381
382    @staticmethod
383    def _FindCvdHostBinaries(search_paths):
384        """Return the directory that contains CVD host binaries."""
385        for search_path in search_paths:
386            if os.path.isfile(os.path.join(search_path, "bin",
387                                           constants.CMD_LAUNCH_CVD)):
388                return search_path
389
390        raise errors.GetCvdLocalHostPackageError(
391            "CVD host binaries are not found. Please run `make hosttar`, or "
392            "set --local-tool to an extracted CVD host package.")
393
394    @staticmethod
395    def _FindCvdHostArtifactsPath(search_paths):
396        """Return the directory that contains CVD host artifacts (in particular
397           webrtc).
398        """
399        for search_path in search_paths:
400            if os.path.isfile(os.path.join(search_path,
401                                           "usr/share/webrtc/certs",
402                                           "server.crt")):
403                return search_path
404
405        raise errors.GetCvdLocalHostPackageError(
406            "CVD host webrtc artifacts are not found. Please run "
407            "`make hosttar`, or set --local-tool to an extracted CVD host "
408            "package.")
409
410    @staticmethod
411    def _VerifyExtractedImgZip(image_dir):
412        """Verify that a path is build output dir or extracted img zip.
413
414        This method checks existence of super image. The file is in img zip
415        but not in target files zip. A cuttlefish instance requires a super
416        image if no system image or OTA tools are given.
417
418        Args:
419            image_dir: The directory to be verified.
420
421        Raises:
422            errors.GetLocalImageError if the directory does not contain the
423            needed file.
424        """
425        if not os.path.isfile(os.path.join(image_dir, _SUPER_IMAGE_NAME)):
426            raise errors.GetLocalImageError(
427                f"Cannot find {_SUPER_IMAGE_NAME} in {image_dir}. The "
428                f"directory is expected to be an extracted img zip or "
429                f"{constants.ENV_ANDROID_PRODUCT_OUT}.")
430
431    @staticmethod
432    def _FindBootOrKernelImages(image_path):
433        """Find boot, vendor_boot, kernel, and initramfs images in a path.
434
435        This method expects image_path to be:
436        - An output directory of a kernel build. It contains a kernel image and
437          initramfs.img.
438        - A generic boot image or its parent directory. The image name is
439          boot-*.img. The directory does not contain vendor_boot.img.
440        - An output directory of a cuttlefish build. It contains boot.img and
441          vendor_boot.img.
442
443        Args:
444            image_path: A path to an image file or an image directory.
445
446        Returns:
447            A tuple of strings, the paths to boot, vendor_boot, kernel, and
448            initramfs images. Each value can be None.
449
450        Raises:
451            errors.GetLocalImageError if image_path does not contain boot or
452            kernel images.
453        """
454        kernel_image_path, initramfs_image_path = cvd_utils.FindKernelImages(
455            image_path)
456        if kernel_image_path and initramfs_image_path:
457            return None, None, kernel_image_path, initramfs_image_path
458
459        boot_image_path, vendor_boot_image_path = cvd_utils.FindBootImages(
460            image_path)
461        if boot_image_path:
462            return boot_image_path, vendor_boot_image_path, None, None
463
464        raise errors.GetLocalImageError(f"{image_path} is not a boot image or "
465                                        f"a directory containing images.")
466
467    def GetImageArtifactsPath(self, avd_spec):
468        """Get image artifacts path.
469
470        This method will check if launch_cvd is exist and return the tuple path
471        (image path and host bins path) where they are located respectively.
472        For remote image, RemoteImageLocalInstance will override this method
473        and return the artifacts path which is extracted and downloaded from
474        remote.
475
476        Args:
477            avd_spec: AVDSpec object that tells us what we're going to create.
478
479        Returns:
480            ArtifactPaths object consisting of image directory and host bins
481            package.
482
483        Raises:
484            errors.GetCvdLocalHostPackageError, errors.GetLocalImageError, or
485            errors.CheckPathError if any artifact is not found.
486        """
487        image_dir = os.path.abspath(avd_spec.local_image_dir)
488        tool_dirs = (avd_spec.local_tool_dirs +
489                     create_common.GetNonEmptyEnvVars(
490                         constants.ENV_ANDROID_SOONG_HOST_OUT,
491                         constants.ENV_ANDROID_HOST_OUT))
492        host_bins_path = self._FindCvdHostBinaries(tool_dirs)
493        host_artifacts_path = self._FindCvdHostArtifactsPath(tool_dirs)
494
495        if avd_spec.local_system_image:
496            misc_info_path = cvd_utils.FindMiscInfo(image_dir)
497            image_dir = cvd_utils.FindImageDir(image_dir)
498            ota_tools_dir = os.path.abspath(
499                ota_tools.FindOtaToolsDir(tool_dirs))
500            system_image_path = create_common.FindSystemImage(
501                avd_spec.local_system_image)
502        else:
503            self._VerifyExtractedImgZip(image_dir)
504            misc_info_path = None
505            ota_tools_dir = None
506            system_image_path = None
507
508        if avd_spec.local_kernel_image:
509            (
510                boot_image_path,
511                vendor_boot_image_path,
512                kernel_image_path,
513                initramfs_image_path,
514            ) = self._FindBootOrKernelImages(
515                os.path.abspath(avd_spec.local_kernel_image))
516        else:
517            boot_image_path = None
518            vendor_boot_image_path = None
519            kernel_image_path = None
520            initramfs_image_path = None
521
522        if avd_spec.local_vendor_image:
523            vendor_image_paths = cvd_utils.FindVendorImages(
524                avd_spec.local_vendor_image)
525            vendor_image_path = vendor_image_paths.vendor
526            vendor_dlkm_image_path = vendor_image_paths.vendor_dlkm
527            odm_image_path = vendor_image_paths.odm
528            odm_dlkm_image_path = vendor_image_paths.odm_dlkm
529        else:
530            vendor_image_path = None
531            vendor_dlkm_image_path = None
532            odm_image_path = None
533            odm_dlkm_image_path = None
534
535        return ArtifactPaths(image_dir, host_bins_path,
536                             host_artifacts=host_artifacts_path,
537                             misc_info=misc_info_path,
538                             ota_tools_dir=ota_tools_dir,
539                             system_image=system_image_path,
540                             boot_image=boot_image_path,
541                             vendor_boot_image=vendor_boot_image_path,
542                             kernel_image=kernel_image_path,
543                             initramfs_image=initramfs_image_path,
544                             vendor_image=vendor_image_path,
545                             vendor_dlkm_image=vendor_dlkm_image_path,
546                             odm_image=odm_image_path,
547                             odm_dlkm_image=odm_dlkm_image_path)
548
549    @staticmethod
550    def _GetConfigFromAndroidInfo(android_info_path):
551        """Get config value from android-info.txt.
552
553        The config in android-info.txt would like "config=phone".
554
555        Args:
556            android_info_path: String of android-info.txt pah.
557
558        Returns:
559            Strings of config value.
560        """
561        if os.path.exists(android_info_path):
562            with open(android_info_path, "r") as android_info_file:
563                android_info = android_info_file.read()
564                logger.debug("Android info: %s", android_info)
565                config_match = _CONFIG_RE.match(android_info)
566                if config_match:
567                    return config_match.group("config")
568        return None
569
570    # pylint: disable=too-many-branches
571    @staticmethod
572    def PrepareLaunchCVDCmd(hw_property, connect_adb, connect_fastboot,
573                            artifact_paths, runtime_dir, connect_webrtc,
574                            connect_vnc, super_image_path, launch_args,
575                            config, openwrt=False, use_launch_cvd=False,
576                            instance_ids=None, webrtc_device_id=None,
577                            vbmeta_image_path=None):
578        """Prepare launch_cvd command.
579
580        Create the launch_cvd commands with all the required args and add
581        in the user groups to it if necessary.
582
583        Args:
584            hw_property: dict object of hw property.
585            artifact_paths: ArtifactPaths object.
586            connect_adb: Boolean flag that enables adb_connector.
587            connect_fastboot: Boolean flag that enables fastboot_proxy.
588            runtime_dir: String of runtime directory path.
589            connect_webrtc: Boolean of connect_webrtc.
590            connect_vnc: Boolean of connect_vnc.
591            super_image_path: String of non-default super image path.
592            launch_args: String of launch args.
593            config: String of config name.
594            openwrt: Boolean of enable OpenWrt devices.
595            use_launch_cvd: Boolean of using launch_cvd for old build cases.
596            instance_ids: List of integer of instance ids.
597            webrtc_device_id: String of webrtc device id.
598            vbmeta_image_path: String of vbmeta image path.
599
600        Returns:
601            String, cvd start cmd.
602        """
603        bin_dir = os.path.join(artifact_paths.host_bins, "bin")
604        cvd_path = os.path.join(bin_dir, constants.CMD_CVD)
605        start_cvd_cmd = cvd_path + _CMD_CVD_START
606        if use_launch_cvd or not os.path.isfile(cvd_path):
607            start_cvd_cmd = os.path.join(bin_dir, constants.CMD_LAUNCH_CVD)
608        launch_cvd_w_args = start_cvd_cmd + _CMD_LAUNCH_CVD_ARGS % (
609            config, artifact_paths.image_dir, runtime_dir)
610        if hw_property:
611            launch_cvd_w_args = launch_cvd_w_args + _CMD_LAUNCH_CVD_HW_ARGS % (
612                hw_property["cpu"], hw_property["x_res"], hw_property["y_res"],
613                hw_property["dpi"], hw_property["memory"])
614            if constants.HW_ALIAS_DISK in hw_property:
615                launch_cvd_w_args = (launch_cvd_w_args +
616                                     _CMD_LAUNCH_CVD_DISK_ARGS %
617                                     hw_property[constants.HW_ALIAS_DISK])
618
619        if not connect_adb:
620            launch_cvd_w_args = launch_cvd_w_args + _CMD_LAUNCH_CVD_NO_ADB_ARG
621
622        if not connect_fastboot:
623            launch_cvd_w_args = launch_cvd_w_args + _CMD_LAUNCH_CVD_NO_FASTBOOT_ARG
624
625        if connect_webrtc:
626            launch_cvd_w_args = launch_cvd_w_args + _CMD_LAUNCH_CVD_WEBRTC_ARGS
627
628        if connect_vnc:
629            launch_cvd_w_args = launch_cvd_w_args + _CMD_LAUNCH_CVD_VNC_ARG
630
631        if super_image_path:
632            launch_cvd_w_args = (launch_cvd_w_args +
633                                 _CMD_LAUNCH_CVD_SUPER_IMAGE_ARG %
634                                 super_image_path)
635
636        if artifact_paths.boot_image:
637            launch_cvd_w_args = (launch_cvd_w_args +
638                                 _CMD_LAUNCH_CVD_BOOT_IMAGE_ARG %
639                                 artifact_paths.boot_image)
640
641        if artifact_paths.vendor_boot_image:
642            launch_cvd_w_args = (launch_cvd_w_args +
643                                 _CMD_LAUNCH_CVD_VENDOR_BOOT_IMAGE_ARG %
644                                 artifact_paths.vendor_boot_image)
645
646        if artifact_paths.kernel_image:
647            launch_cvd_w_args = (launch_cvd_w_args +
648                                 _CMD_LAUNCH_CVD_KERNEL_IMAGE_ARG %
649                                 artifact_paths.kernel_image)
650
651        if artifact_paths.initramfs_image:
652            launch_cvd_w_args = (launch_cvd_w_args +
653                                 _CMD_LAUNCH_CVD_INITRAMFS_IMAGE_ARG %
654                                 artifact_paths.initramfs_image)
655
656        if vbmeta_image_path:
657            launch_cvd_w_args = (launch_cvd_w_args +
658                                 _CMD_LAUNCH_CVD_VBMETA_IMAGE_ARG %
659                                 vbmeta_image_path)
660
661        if openwrt:
662            launch_cvd_w_args = launch_cvd_w_args + _CMD_LAUNCH_CVD_CONSOLE_ARG
663
664        if instance_ids and len(instance_ids) > 1:
665            launch_cvd_w_args = (
666                launch_cvd_w_args +
667                _CMD_LAUNCH_CVD_INSTANCE_NUMS_ARG %
668                ",".join(map(str, instance_ids)))
669
670        if webrtc_device_id:
671            launch_cvd_w_args = (launch_cvd_w_args +
672                                 _CMD_LAUNCH_CVD_WEBRTC_DEIVE_ID %
673                                 webrtc_device_id)
674
675        if launch_args:
676            launch_cvd_w_args = launch_cvd_w_args + " " + launch_args
677
678        launch_cmd = utils.AddUserGroupsToCmd(launch_cvd_w_args,
679                                              constants.LIST_CF_USER_GROUPS)
680        logger.debug("launch_cvd cmd:\n %s", launch_cmd)
681        return launch_cmd
682
683    @staticmethod
684    def PrepareLocalCvdToolsLink(cvd_home_dir, host_bins_path):
685        """Create symbolic link for the cvd tools directory.
686
687        local instance's cvd tools could be generated in /out after local build
688        or be generated in the download image folder. It creates a symbolic
689        link then only check cvd_status using known link for both cases.
690
691        Args:
692            cvd_home_dir: The parent directory of the link
693            host_bins_path: String of host package directory.
694
695        Returns:
696            String of cvd_tools link path
697        """
698        cvd_tools_link_path = os.path.join(cvd_home_dir,
699                                           constants.CVD_TOOLS_LINK_NAME)
700        if os.path.islink(cvd_tools_link_path):
701            os.unlink(cvd_tools_link_path)
702        os.symlink(host_bins_path, cvd_tools_link_path)
703        return cvd_tools_link_path
704
705    @staticmethod
706    def _TrustCertificatesForWebRTC(host_bins_path):
707        """Copy the trusted certificates generated by openssl tool to the
708        webrtc frontend certificate directory.
709
710        Args:
711            host_bins_path: String of host package directory.
712        """
713        webrtc_certs_dir = os.path.join(host_bins_path,
714                                        constants.WEBRTC_CERTS_PATH)
715        if not os.path.isdir(webrtc_certs_dir):
716            logger.debug("WebRTC frontend certificate path doesn't exist: %s",
717                         webrtc_certs_dir)
718            return
719        local_cert_dir = os.path.join(os.path.expanduser("~"),
720                                      constants.SSL_DIR)
721        if mkcert.AllocateLocalHostCert():
722            for cert_file_name in constants.WEBRTC_CERTS_FILES:
723                shutil.copyfile(
724                    os.path.join(local_cert_dir, cert_file_name),
725                    os.path.join(webrtc_certs_dir, cert_file_name))
726
727    @staticmethod
728    def _LogCvdVersion(host_bins_path):
729        """Log the version of the cvd server.
730
731        Args:
732            host_bins_path: String of host package directory.
733        """
734        cvd_path = os.path.join(host_bins_path, "bin", constants.CMD_CVD)
735        if not os.path.isfile(cvd_path):
736            logger.info("Skip logging cvd version as %s is not a file",
737                        cvd_path)
738            return
739
740        cmd = cvd_path + _CMD_CVD_VERSION
741        try:
742            proc = subprocess.run(cmd, shell=True, text=True,
743                                  capture_output=True, timeout=5,
744                                  check=False, cwd=host_bins_path)
745            logger.info("`%s` returned %d; stdout:\n%s",
746                        cmd, proc.returncode, proc.stdout)
747            logger.info("`%s` stderr:\n%s", cmd, proc.stderr)
748        except subprocess.SubprocessError as e:
749            logger.error("`%s` failed: %s", cmd, e)
750
751    @staticmethod
752    def _CheckRunningCvd(local_instance_id, no_prompts=False):
753        """Check if launch_cvd with the same instance id is running.
754
755        Args:
756            local_instance_id: Integer of instance id.
757            no_prompts: Boolean, True to skip all prompts.
758
759        Returns:
760            Whether the user wants to continue.
761        """
762        # Check if the instance with same id is running.
763        existing_ins = list_instance.GetActiveCVD(local_instance_id)
764        if existing_ins:
765            if no_prompts or utils.GetUserAnswerYes(_CONFIRM_RELAUNCH %
766                                                    local_instance_id):
767                existing_ins.Delete()
768            else:
769                return False
770        return True
771
772    @staticmethod
773    def _StopCvd(local_instance_id, proc):
774        """Call stop_cvd or kill a launch_cvd process.
775
776        Args:
777            local_instance_id: Integer of instance id.
778            proc: subprocess.Popen object, the launch_cvd process.
779        """
780        existing_ins = list_instance.GetActiveCVD(local_instance_id)
781        if existing_ins:
782            try:
783                existing_ins.Delete()
784                return
785            except subprocess.CalledProcessError as e:
786                logger.error("Cannot stop instance %d: %s",
787                             local_instance_id, str(e))
788        else:
789            logger.error("Instance %d is not active.", local_instance_id)
790        logger.info("Terminate launch_cvd process.")
791        proc.terminate()
792
793    @utils.TimeExecute(function_description="Waiting for AVD(s) to boot up")
794    def _LaunchCvd(self, cmd, local_instance_id, host_bins_path,
795                   host_artifacts_path, cvd_home_dir, timeout):
796        """Execute Launch CVD.
797
798        Kick off the launch_cvd command and log the output.
799
800        Args:
801            cmd: String, launch_cvd command.
802            local_instance_id: Integer of instance id.
803            host_bins_path: String of host package directory containing
804              binaries.
805            host_artifacts_path: String of host package directory containing
806              other artifacts.
807            cvd_home_dir: String, the home directory for the instance.
808            timeout: Integer, the number of seconds to wait for the AVD to
809              boot up.
810
811        Raises:
812            errors.LaunchCVDFail if launch_cvd times out or returns non-zero.
813        """
814        cvd_env = os.environ.copy()
815        cvd_env[constants.ENV_ANDROID_SOONG_HOST_OUT] = host_artifacts_path
816        # launch_cvd assumes host bins are in $ANDROID_HOST_OUT.
817        cvd_env[constants.ENV_ANDROID_HOST_OUT] = host_bins_path
818        cvd_env[constants.ENV_CVD_HOME] = cvd_home_dir
819        cvd_env[constants.ENV_CUTTLEFISH_INSTANCE] = str(local_instance_id)
820        cvd_env[constants.ENV_CUTTLEFISH_CONFIG_FILE] = (
821            instance.GetLocalInstanceConfigPath(local_instance_id))
822        cvd_env[constants.ENV_CVD_ACQUIRE_FILE_LOCK] = "false"
823        stdout_file = os.path.join(cvd_home_dir, _STDOUT)
824        stderr_file = os.path.join(cvd_home_dir, _STDERR)
825        # Check the result of launch_cvd command.
826        # An exit code of 0 is equivalent to VIRTUAL_DEVICE_BOOT_COMPLETED
827        with open(stdout_file, "w+") as f_stdout, open(stderr_file,
828                                                       "w+") as f_stderr:
829            try:
830                proc = subprocess.Popen(
831                    cmd, shell=True, env=cvd_env, stdout=f_stdout,
832                    stderr=f_stderr, text=True, cwd=host_bins_path)
833                proc.communicate(timeout=timeout)
834                f_stdout.seek(0)
835                f_stderr.seek(0)
836                if proc.returncode == 0:
837                    logger.info("launch_cvd stdout:\n%s", f_stdout.read())
838                    logger.info("launch_cvd stderr:\n%s", f_stderr.read())
839                    return
840                error_msg = "launch_cvd returned %d." % proc.returncode
841            except subprocess.TimeoutExpired:
842                self._StopCvd(local_instance_id, proc)
843                proc.communicate(timeout=5)
844                error_msg = "Device did not boot within %d secs." % timeout
845
846            f_stdout.seek(0)
847            f_stderr.seek(0)
848            stderr = f_stderr.read()
849            logger.error("launch_cvd stdout:\n%s", f_stdout.read())
850            logger.error("launch_cvd stderr:\n%s", stderr)
851            split_stderr = stderr.splitlines()[-_MAX_REPORTED_ERROR_LINES:]
852            raise errors.LaunchCVDFail(
853                "%s Stderr:\n%s" % (error_msg, "\n".join(split_stderr)))
854