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