1#
2# Copyright (C) 2018 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the 'License');
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#      http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an 'AS IS' BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#
16
17import httplib2
18import logging
19import socket
20import threading
21import time
22
23from googleapiclient import errors
24
25from host_controller import common
26from host_controller.command_processor import base_command_processor
27from host_controller.console_argument_parser import ConsoleArgumentError
28from host_controller.tradefed import remote_operation
29from host_controller.utils.usb import usb_utils
30
31from vts.utils.python.common import cmd_utils
32
33
34class CommandDevice(base_command_processor.BaseCommandProcessor):
35    """Command processor for Device command.
36
37    Attributes:
38        arg_parser: ConsoleArgumentParser object, argument parser.
39        console: cmd.Cmd console object.
40        command: string, command name which this processor will handle.
41        command_detail: string, detailed explanation for the command.
42        update_thread: threading.Thread that updates device state regularly.
43    """
44
45    command = "device"
46    command_detail = "Selects device(s) under test."
47
48    def UpdateDevice(self,
49                     server_type,
50                     host,
51                     lease,
52                     suppress_lock_warning=True,
53                     from_job_pool=False):
54        """Updates the device state of all devices on a given host.
55
56        Args:
57            server_type: string, the type of a test secheduling server.
58            host: HostController object
59            lease: boolean, True to lease and execute jobs.
60            suppress_lock_warning: bool, True to suppress the warning msg from
61                                   file_lock.
62            from_job_pool: bool, True if the 'device' command is executed from
63                           one of the job pool processes. Checks only
64                           the availability of the devices when set.
65        """
66        if server_type == "vti":
67            devices = []
68
69            if from_job_pool:
70                devices_dict = {}
71                for serial in self.console.GetSerials():
72                    device = {}
73                    device["serial"] = serial
74                    device["status"] = common._DEVICE_STATUS_DICT[
75                        "no-response"]
76                    device["product"] = "error"
77                    devices_dict[serial] = device
78
79            stdout, stderr, returncode = cmd_utils.ExecuteOneShellCommand(
80                "adb devices")
81            lines_adb = stdout.split("\n")
82            stdout, stderr, returncode = cmd_utils.ExecuteOneShellCommand(
83                "fastboot devices")
84            lines_fastboot = stdout.split("\n")
85
86            for line in lines_adb:
87                if (len(line.strip()) and not (line.startswith("* ")
88                                               or line.startswith("List "))):
89                    device = {}
90                    device["serial"] = line.split()[0]
91                    serial = device["serial"]
92
93                    if from_job_pool:
94                        if (serial in devices_dict
95                                and line.split()[1] == "device"):
96                            devices_dict[serial][
97                                "status"] = common._DEVICE_STATUS_DICT[
98                                    "online"]
99                            product = (self.console._vti_endpoint_client.
100                                       GetJobDeviceProductName())
101                            if product:
102                                devices_dict[serial]["product"] = product
103                        continue
104
105                    if self.console.file_lock.LockDevice(
106                            serial, suppress_lock_warning) == False:
107                        self.console.device_status[
108                            serial] = common._DEVICE_STATUS_DICT["use"]
109                        if not suppress_lock_warning:
110                            logging.info("Device %s already locked." % serial)
111                        continue
112
113                    stdout, _, retcode = cmd_utils.ExecuteOneShellCommand(
114                        "adb -s %s reboot bootloader" % device["serial"],
115                        common.DEFAULT_DEVICE_TIMEOUT_SECS,
116                        usb_utils.ResetUsbDeviceOfSerial_Callback,
117                        device["serial"])
118                    if retcode == 0:
119                        lines_fastboot.append(line)
120
121                    self.console.file_lock.UnlockDevice(serial)
122
123            for line in lines_fastboot:
124                if len(line.strip()):
125                    device = {}
126                    device["serial"] = line.split()[0]
127                    serial = device["serial"]
128
129                    if from_job_pool:
130                        if serial in devices_dict:
131                            devices_dict[serial][
132                                "status"] = common._DEVICE_STATUS_DICT[
133                                    "fastboot"]
134                            product = (self.console._vti_endpoint_client.
135                                       GetJobDeviceProductName())
136                            if product:
137                                devices_dict[serial]["product"] = product
138                        continue
139
140                    if self.console.file_lock.LockDevice(
141                            serial, suppress_lock_warning) == False:
142                        self.console.device_status[
143                            serial] = common._DEVICE_STATUS_DICT["use"]
144                        if not suppress_lock_warning:
145                            logging.info("Device %s already locked." % serial)
146                        continue
147
148                    _, stderr, retcode = cmd_utils.ExecuteOneShellCommand(
149                        "fastboot -s %s getvar product" % device["serial"],
150                        common.DEFAULT_DEVICE_TIMEOUT_SECS,
151                        usb_utils.ResetUsbDeviceOfSerial_Callback,
152                        device["serial"])
153                    if retcode == 0:
154                        res = stderr.splitlines()[0].rstrip()
155                        if ":" in res:
156                            device["product"] = res.split(":")[1].strip()
157                        elif "waiting for %s" % serial in res:
158                            res = stderr.splitlines()[1].rstrip()
159                            device["product"] = res.split(":")[1].strip()
160                        else:
161                            device["product"] = "error"
162                        self.console.device_status[
163                            serial] = common._DEVICE_STATUS_DICT["fastboot"]
164                    else:
165                        device["product"] = "error"
166                        self.console.device_status[
167                            serial] = common._DEVICE_STATUS_DICT["no-response"]
168
169                    device["status"] = self.console.device_status[serial]
170                    devices.append(device)
171
172                    self.console.file_lock.UnlockDevice(serial)
173
174            if from_job_pool:
175                devices = devices_dict.values()
176                if devices:
177                    self.console._vti_endpoint_client.UploadDeviceInfo(
178                        host.hostname, devices)
179                return
180
181            self.console._vti_endpoint_client.UploadDeviceInfo(
182                host.hostname, devices)
183
184            if lease:
185                self.console._job_in_queue.put("lease")
186
187            if self.console.vtslab_version:
188                self.console._vti_endpoint_client.UploadHostVersion(
189                    host.hostname, self.console.vtslab_version)
190        elif server_type == "tfc":
191            devices = host.ListDevices()
192            for device in devices:
193                device.Extend(['sim_state', 'sim_operator', 'mac_address'])
194            snapshots = self.console._tfc_client.CreateDeviceSnapshot(
195                host._cluster_ids[0], host.hostname, devices)
196            self.console._tfc_client.SubmitHostEvents([snapshots])
197        else:
198            logging.error("Error: unknown server_type %s for UpdateDevice",
199                          server_type)
200
201    def UpdateDeviceRepeat(self,
202                           server_type,
203                           host,
204                           lease,
205                           update_interval,
206                           suppress_lock_warning=True,
207                           from_job_pool=False):
208        """Regularly updates the device state of devices on a given host.
209
210        Args:
211            server_type: string, the type of a test secheduling server.
212            host: HostController object
213            lease: boolean, True to lease and execute jobs.
214            update_interval: int, number of seconds before repeating
215            suppress_lock_warning: bool, True to suppress the warning msg from
216                                   file_lock.
217            from_job_pool: bool, True if the 'device' command is executed form
218                           one of the job pool processes.
219        """
220        thread = threading.currentThread()
221        while getattr(thread, 'keep_running', True):
222            try:
223                self.UpdateDevice(server_type, host, lease,
224                                  suppress_lock_warning, from_job_pool)
225            except (socket.error, remote_operation.RemoteOperationException,
226                    httplib2.HttpLib2Error, errors.HttpError) as e:
227                logging.exception(e)
228            time.sleep(update_interval)
229
230    def RunUSBResetTimer(self, serial, interval):
231        """Sets up a timer to run the target function after 'interval' secs.
232
233        Args:
234            serial: string, serial number of the device whose USB device file
235                    will reset when the timeout happens.
236            interval: int, sets up the timer for the target function to be
237                      executed after 'interval' seconds, if not canceled.
238
239        Returns:
240            threading.Timer, set to reset USB port corresponding the device
241            with the given serial number.
242        """
243        usb_reset_timer = threading.Timer(interval, self.USBResetCallback,
244                                          (serial, ))
245        usb_reset_timer.daemon = True
246        usb_reset_timer.start()
247
248        return usb_reset_timer
249
250    def USBResetCallback(self, serial):
251        """Resets USB device file corresponding to the given device serial.
252
253        Args:
254            serial: string, serial number of the device whose USB device file
255                    will reset.
256        """
257        device_file_path = usb_utils.GetDevicesUSBFilePath()
258        if serial in device_file_path:
259            logging.error(
260                "Device %s not responding. Resetting device file %s.", serial,
261                device_file_path[serial])
262            usb_utils.ResetDeviceUsb(device_file_path[serial])
263
264    # @Override
265    def SetUp(self):
266        """Initializes the parser for device command."""
267        self.update_thread = None
268        self.arg_parser.add_argument(
269            "--set_serial",
270            default="",
271            help="Serial number for device. Can be a comma-separated list.")
272        self.arg_parser.add_argument(
273            "--update",
274            choices=("single", "start", "stop"),
275            default="start",
276            help="Update device info on cloud scheduler")
277        self.arg_parser.add_argument(
278            "--interval",
279            type=int,
280            default=30,
281            help="Interval (seconds) to repeat device update.")
282        self.arg_parser.add_argument(
283            "--host", type=int, help="The index of the host.")
284        self.arg_parser.add_argument(
285            "--server_type",
286            choices=("vti", "tfc"),
287            default="vti",
288            help="The type of a cloud-based test scheduler server.")
289        self.arg_parser.add_argument(
290            "--lease",
291            default=False,
292            type=bool,
293            help="Whether to lease jobs and execute them.")
294        self.arg_parser.add_argument(
295            "--suppress_lock_warning",
296            default=True,
297            help="Whether to suppress device lock warning messages.")
298        self.arg_parser.add_argument(
299            "--from_job_pool",
300            action="store_true",
301            help="Whether the command is executed from the job pool. "
302            "Check only the availability of the devices when set.")
303
304    # @Override
305    def Run(self, arg_line):
306        """Sets device info such as serial number."""
307        args = self.arg_parser.ParseLine(arg_line)
308        if args.set_serial:
309            self.console.SetSerials(args.set_serial.split(","))
310            logging.info("serials: %s", self.console._serials)
311        if args.update:
312            if args.host is None:
313                if len(self.console._hosts) > 1:
314                    raise ConsoleArgumentError("More than one host.")
315                args.host = 0
316            host = self.console._hosts[args.host]
317
318            if args.suppress_lock_warning:
319                if (type(args.suppress_lock_warning) != str
320                        or args.suppress_lock_warning.lower() == "true"):
321                    suppress_lock_warning = True
322                else:
323                    suppress_lock_warning = False
324
325            if args.update == "single":
326                self.UpdateDevice(args.server_type, host, args.lease,
327                                  suppress_lock_warning, args.from_job_pool)
328            elif args.update == "start":
329                if args.interval <= 0:
330                    raise ConsoleArgumentError(
331                        "update interval must be positive")
332                # do not allow user to create new
333                # thread if one is currently running
334                if self.update_thread is not None and not hasattr(
335                        self.update_thread, 'keep_running'):
336                    logging.warning('device update already running. '
337                                    'run device --update stop first.')
338                    return
339                self.update_thread = threading.Thread(
340                    target=self.UpdateDeviceRepeat,
341                    args=(
342                        args.server_type,
343                        host,
344                        args.lease,
345                        args.interval,
346                        suppress_lock_warning,
347                        args.from_job_pool,
348                    ))
349                self.update_thread.daemon = True
350                self.update_thread.start()
351            elif args.update == "stop":
352                self.update_thread.keep_running = False
353                if self.console.GetSerials():
354                    self.console.ResetSerials()
355