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