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.
25Acloud sets the other 2 variables for each local instance.
26
27The adb port and vnc port of local instance will be decided according to
28instance id. The rule of adb port will be '6520 + [instance id] - 1' and the vnc
29port will be '6444 + [instance id] - 1'.
30e.g:
31If instance id = 3 the adb port will be 6522 and vnc port will be 6446.
32
33To delete the local instance, we will call stop_cvd with the environment variable
34[CUTTLEFISH_CONFIG_FILE] which is pointing to the runtime cuttlefish json.
35"""
36
37import logging
38import os
39import shutil
40import subprocess
41import threading
42import sys
43
44from acloud import errors
45from acloud.create import base_avd_create
46from acloud.internal import constants
47from acloud.internal.lib import utils
48from acloud.internal.lib.adb_tools import AdbTools
49from acloud.list import list as list_instance
50from acloud.list import instance
51from acloud.public import report
52
53
54logger = logging.getLogger(__name__)
55
56_CMD_LAUNCH_CVD_ARGS = (" -daemon -cpus %s -x_res %s -y_res %s -dpi %s "
57                        "-memory_mb %s -run_adb_connector=%s "
58                        "-system_image_dir %s -instance_dir %s "
59                        "-undefok=report_anonymous_usage_stats,enable_sandbox "
60                        "-report_anonymous_usage_stats=y "
61                        "-enable_sandbox=false")
62_CMD_LAUNCH_CVD_GPU_ARG = " -gpu_mode=drm_virgl"
63_CMD_LAUNCH_CVD_DISK_ARGS = (" -blank_data_image_mb %s "
64                             "-data_policy always_create")
65_CMD_LAUNCH_CVD_WEBRTC_ARGS = (" -guest_enforce_security=false "
66                               "-vm_manager=crosvm "
67                               "-start_webrtc=true "
68                               "-webrtc_public_ip=%s" % constants.LOCALHOST)
69_CONFIRM_RELAUNCH = ("\nCuttlefish AVD[id:%d] is already running. \n"
70                     "Enter 'y' to terminate current instance and launch a new "
71                     "instance, enter anything else to exit out[y/N]: ")
72_LAUNCH_CVD_TIMEOUT_ERROR = ("Cuttlefish AVD launch timeout, did not complete "
73                             "within %d secs.")
74_VIRTUAL_DISK_PATHS = "virtual_disk_paths"
75
76
77class LocalImageLocalInstance(base_avd_create.BaseAVDCreate):
78    """Create class for a local image local instance AVD."""
79
80    @utils.TimeExecute(function_description="Total time: ",
81                       print_before_call=False, print_status=False)
82    def _CreateAVD(self, avd_spec, no_prompts):
83        """Create the AVD.
84
85        Args:
86            avd_spec: AVDSpec object that tells us what we're going to create.
87            no_prompts: Boolean, True to skip all prompts.
88
89        Raises:
90            errors.LaunchCVDFail: Launch AVD failed.
91
92        Returns:
93            A Report instance.
94        """
95        # Running instances on local is not supported on all OS.
96        if not utils.IsSupportedPlatform(print_warning=True):
97            result_report = report.Report(command="create")
98            result_report.SetStatus(report.Status.FAIL)
99            return result_report
100
101        local_image_path, host_bins_path = self.GetImageArtifactsPath(avd_spec)
102
103        launch_cvd_path = os.path.join(host_bins_path, "bin",
104                                       constants.CMD_LAUNCH_CVD)
105        cmd = self.PrepareLaunchCVDCmd(launch_cvd_path,
106                                       avd_spec.hw_property,
107                                       avd_spec.connect_adb,
108                                       local_image_path,
109                                       avd_spec.local_instance_id,
110                                       avd_spec.connect_webrtc,
111                                       avd_spec.gpu)
112
113        result_report = report.Report(command="create")
114        instance_name = instance.GetLocalInstanceName(
115            avd_spec.local_instance_id)
116        try:
117            self.CheckLaunchCVD(
118                cmd, host_bins_path, avd_spec.local_instance_id,
119                local_image_path, avd_spec.connect_webrtc, no_prompts,
120                avd_spec.boot_timeout_secs or constants.DEFAULT_CF_BOOT_TIMEOUT)
121        except errors.LaunchCVDFail as launch_error:
122            result_report.SetStatus(report.Status.BOOT_FAIL)
123            result_report.AddDeviceBootFailure(
124                instance_name, constants.LOCALHOST, None, None,
125                error=str(launch_error))
126            return result_report
127
128        active_ins = list_instance.GetActiveCVD(avd_spec.local_instance_id)
129        if active_ins:
130            result_report.SetStatus(report.Status.SUCCESS)
131            result_report.AddDevice(instance_name, constants.LOCALHOST,
132                                    active_ins.adb_port, active_ins.vnc_port)
133            # Launch vnc client if we're auto-connecting.
134            if avd_spec.connect_vnc:
135                utils.LaunchVNCFromReport(result_report, avd_spec, no_prompts)
136            if avd_spec.connect_webrtc:
137                utils.LaunchBrowserFromReport(result_report)
138            if avd_spec.unlock_screen:
139                AdbTools(active_ins.adb_port).AutoUnlockScreen()
140        else:
141            err_msg = "cvd_status return non-zero after launch_cvd"
142            logger.error(err_msg)
143            result_report.SetStatus(report.Status.BOOT_FAIL)
144            result_report.AddDeviceBootFailure(
145                instance_name, constants.LOCALHOST, None, None, error=err_msg)
146        return result_report
147
148    @staticmethod
149    def _FindCvdHostBinaries(search_paths):
150        """Return the directory that contains CVD host binaries."""
151        for search_path in search_paths:
152            if os.path.isfile(os.path.join(search_path, "bin",
153                                           constants.CMD_LAUNCH_CVD)):
154                return search_path
155
156        host_out_dir = os.environ.get(constants.ENV_ANDROID_HOST_OUT)
157        if (host_out_dir and
158                os.path.isfile(os.path.join(host_out_dir, "bin",
159                                            constants.CMD_LAUNCH_CVD))):
160            return host_out_dir
161
162        raise errors.GetCvdLocalHostPackageError(
163            "CVD host binaries are not found. Please run `make hosttar`, or "
164            "set --local-tool to an extracted CVD host package.")
165
166    def GetImageArtifactsPath(self, avd_spec):
167        """Get image artifacts path.
168
169        This method will check if launch_cvd is exist and return the tuple path
170        (image path and host bins path) where they are located respectively.
171        For remote image, RemoteImageLocalInstance will override this method and
172        return the artifacts path which is extracted and downloaded from remote.
173
174        Args:
175            avd_spec: AVDSpec object that tells us what we're going to create.
176
177        Returns:
178            Tuple of (local image file, host bins package) paths.
179        """
180        return (avd_spec.local_image_dir,
181                self._FindCvdHostBinaries(avd_spec.local_tool_dirs))
182
183    @staticmethod
184    def PrepareLaunchCVDCmd(launch_cvd_path, hw_property, connect_adb,
185                            system_image_dir, local_instance_id, connect_webrtc,
186                            gpu):
187        """Prepare launch_cvd command.
188
189        Create the launch_cvd commands with all the required args and add
190        in the user groups to it if necessary.
191
192        Args:
193            launch_cvd_path: String of launch_cvd path.
194            hw_property: dict object of hw property.
195            system_image_dir: String of local images path.
196            connect_adb: Boolean flag that enables adb_connector.
197            local_instance_id: Integer of instance id.
198            connect_webrtc: Boolean of connect_webrtc.
199            gpu: String of gpu name, the gpu name of local instance should be
200                 "default" if gpu is enabled.
201
202        Returns:
203            String, launch_cvd cmd.
204        """
205        instance_dir = instance.GetLocalInstanceRuntimeDir(local_instance_id)
206        launch_cvd_w_args = launch_cvd_path + _CMD_LAUNCH_CVD_ARGS % (
207            hw_property["cpu"], hw_property["x_res"], hw_property["y_res"],
208            hw_property["dpi"], hw_property["memory"],
209            ("true" if connect_adb else "false"), system_image_dir,
210            instance_dir)
211        if constants.HW_ALIAS_DISK in hw_property:
212            launch_cvd_w_args = (launch_cvd_w_args + _CMD_LAUNCH_CVD_DISK_ARGS %
213                                 hw_property[constants.HW_ALIAS_DISK])
214        if connect_webrtc:
215            launch_cvd_w_args = launch_cvd_w_args + _CMD_LAUNCH_CVD_WEBRTC_ARGS
216
217        if gpu:
218            launch_cvd_w_args = launch_cvd_w_args + _CMD_LAUNCH_CVD_GPU_ARG
219
220        launch_cmd = utils.AddUserGroupsToCmd(launch_cvd_w_args,
221                                              constants.LIST_CF_USER_GROUPS)
222        logger.debug("launch_cvd cmd:\n %s", launch_cmd)
223        return launch_cmd
224
225    def CheckLaunchCVD(self, cmd, host_bins_path, local_instance_id,
226                       local_image_path, connect_webrtc=False, no_prompts=False,
227                       timeout_secs=constants.DEFAULT_CF_BOOT_TIMEOUT):
228        """Execute launch_cvd command and wait for boot up completed.
229
230        1. Check if the provided image files are in use by any launch_cvd process.
231        2. Check if launch_cvd with the same instance id is running.
232        3. Launch local AVD.
233
234        Args:
235            cmd: String, launch_cvd command.
236            host_bins_path: String of host package directory.
237            local_instance_id: Integer of instance id.
238            local_image_path: String of local image directory.
239            connect_webrtc: Boolean, whether to auto connect webrtc to device.
240            no_prompts: Boolean, True to skip all prompts.
241            timeout_secs: Integer, the number of seconds to wait for the AVD to boot up.
242        """
243        # launch_cvd assumes host bins are in $ANDROID_HOST_OUT, let's overwrite
244        # it to wherever we're running launch_cvd since they could be in a
245        # different dir (e.g. downloaded image).
246        os.environ[constants.ENV_ANDROID_HOST_OUT] = host_bins_path
247        # Check if the instance with same id is running.
248        existing_ins = list_instance.GetActiveCVD(local_instance_id)
249        if existing_ins:
250            if no_prompts or utils.GetUserAnswerYes(_CONFIRM_RELAUNCH %
251                                                    local_instance_id):
252                existing_ins.Delete()
253            else:
254                sys.exit(constants.EXIT_BY_USER)
255        else:
256            # Image files can't be shared among instances, so check if any running
257            # launch_cvd process is using this path.
258            occupied_ins_id = self.IsLocalImageOccupied(local_image_path)
259            if occupied_ins_id:
260                utils.PrintColorString(
261                    "The image path[%s] is already used by current running AVD"
262                    "[id:%d]\nPlease choose another path to launch local "
263                    "instance." % (local_image_path, occupied_ins_id),
264                    utils.TextColors.FAIL)
265                sys.exit(constants.EXIT_BY_USER)
266        if connect_webrtc:
267            utils.ReleasePort(constants.WEBRTC_LOCAL_PORT)
268        self._LaunchCvd(cmd, local_instance_id, timeout=timeout_secs)
269
270    @staticmethod
271    @utils.TimeExecute(function_description="Waiting for AVD(s) to boot up")
272    def _LaunchCvd(cmd, local_instance_id, timeout=None):
273        """Execute Launch CVD.
274
275        Kick off the launch_cvd command and log the output.
276
277        Args:
278            cmd: String, launch_cvd command.
279            local_instance_id: Integer of instance id.
280            timeout: Integer, the number of seconds to wait for the AVD to boot up.
281
282        Raises:
283            errors.LaunchCVDFail when any CalledProcessError.
284        """
285        # Delete the cvd home/runtime temp if exist. The runtime folder is
286        # under the cvd home dir, so we only delete them from home dir.
287        cvd_home_dir = instance.GetLocalInstanceHomeDir(local_instance_id)
288        cvd_runtime_dir = instance.GetLocalInstanceRuntimeDir(local_instance_id)
289        shutil.rmtree(cvd_home_dir, ignore_errors=True)
290        os.makedirs(cvd_runtime_dir)
291
292        cvd_env = os.environ.copy()
293        cvd_env[constants.ENV_CVD_HOME] = cvd_home_dir
294        cvd_env[constants.ENV_CUTTLEFISH_INSTANCE] = str(local_instance_id)
295        # Check the result of launch_cvd command.
296        # An exit code of 0 is equivalent to VIRTUAL_DEVICE_BOOT_COMPLETED
297        process = subprocess.Popen(cmd, shell=True, stderr=subprocess.STDOUT,
298                                   env=cvd_env)
299        if timeout:
300            timer = threading.Timer(timeout, process.kill)
301            timer.start()
302        process.wait()
303        if timeout:
304            timer.cancel()
305        if process.returncode == 0:
306            return
307        raise errors.LaunchCVDFail(
308            "Can't launch cuttlefish AVD. Return code:%s. \nFor more detail: "
309            "%s/launcher.log" % (str(process.returncode), cvd_runtime_dir))
310
311    @staticmethod
312    def IsLocalImageOccupied(local_image_dir):
313        """Check if the given image path is being used by a running CVD process.
314
315        Args:
316            local_image_dir: String, path of local image.
317
318        Return:
319            Integer of instance id which using the same image path.
320        """
321        # TODO(149602560): Remove occupied image checking after after cf disk
322        # overlay is stable
323        for cf_runtime_config_path in instance.GetAllLocalInstanceConfigs():
324            ins = instance.LocalInstance(cf_runtime_config_path)
325            if ins.CvdStatus():
326                for disk_path in ins.virtual_disk_paths:
327                    if local_image_dir in disk_path:
328                        return ins.instance_id
329        return None
330