1#!/usr/bin/env python3
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.
16
17import backoff
18import json
19import logging
20import platform
21import os
22import random
23import re
24import requests
25import subprocess
26import socket
27import time
28
29from acts import context
30from acts import logger as acts_logger
31from acts import utils
32from acts import signals
33
34from acts.controllers.fuchsia_lib.bt.avdtp_lib import FuchsiaAvdtpLib
35from acts.controllers.fuchsia_lib.bt.ble_lib import FuchsiaBleLib
36from acts.controllers.fuchsia_lib.bt.btc_lib import FuchsiaBtcLib
37from acts.controllers.fuchsia_lib.bt.gattc_lib import FuchsiaGattcLib
38from acts.controllers.fuchsia_lib.bt.gatts_lib import FuchsiaGattsLib
39from acts.controllers.fuchsia_lib.bt.sdp_lib import FuchsiaProfileServerLib
40from acts.controllers.fuchsia_lib.hwinfo_lib import FuchsiaHwinfoLib
41from acts.controllers.fuchsia_lib.logging_lib import FuchsiaLoggingLib
42from acts.controllers.fuchsia_lib.netstack.netstack_lib import FuchsiaNetstackLib
43from acts.controllers.fuchsia_lib.syslog_lib import FuchsiaSyslogError
44from acts.controllers.fuchsia_lib.syslog_lib import start_syslog
45from acts.controllers.fuchsia_lib.utils_lib import create_ssh_connection
46from acts.controllers.fuchsia_lib.utils_lib import SshResults
47from acts.controllers.fuchsia_lib.wlan_lib import FuchsiaWlanLib
48from acts.libs.proc.job import Error
49
50ACTS_CONTROLLER_CONFIG_NAME = "FuchsiaDevice"
51ACTS_CONTROLLER_REFERENCE_NAME = "fuchsia_devices"
52
53FUCHSIA_DEVICE_EMPTY_CONFIG_MSG = "Configuration is empty, abort!"
54FUCHSIA_DEVICE_NOT_LIST_CONFIG_MSG = "Configuration should be a list, abort!"
55FUCHSIA_DEVICE_INVALID_CONFIG = ("Fuchsia device config must be either a str "
56                                 "or dict. abort! Invalid element %i in %r")
57FUCHSIA_DEVICE_NO_IP_MSG = "No IP address specified, abort!"
58FUCHSIA_COULD_NOT_GET_DESIRED_STATE = "Could not %s %s."
59FUCHSIA_INVALID_CONTROL_STATE = "Invalid control state (%s). abort!"
60FUCHSIA_SSH_CONFIG_NOT_DEFINED = ("Cannot send ssh commands since the "
61                                  "ssh_config was not specified in the Fuchsia"
62                                  "device config.")
63
64FUCHSIA_SSH_USERNAME = "fuchsia"
65FUCHSIA_TIME_IN_NANOSECONDS = 1000000000
66
67SL4F_APK_NAME = "com.googlecode.android_scripting"
68DAEMON_INIT_TIMEOUT_SEC = 1
69
70DAEMON_ACTIVATED_STATES = ["running", "start"]
71DAEMON_DEACTIVATED_STATES = ["stop", "stopped"]
72
73FUCHSIA_DEFAULT_LOG_CMD = 'iquery --absolute_paths --cat --format= --recursive'
74FUCHSIA_DEFAULT_LOG_ITEMS = [
75    '/hub/c/scenic.cmx/[0-9]*/out/objects',
76    '/hub/c/root_presenter.cmx/[0-9]*/out/objects',
77    '/hub/c/wlanstack2.cmx/[0-9]*/out/public',
78    '/hub/c/basemgr.cmx/[0-9]*/out/objects'
79]
80
81FUCHSIA_RECONNECT_AFTER_REBOOT_TIME = 5
82
83ENABLE_LOG_LISTENER = True
84
85CHANNEL_OPEN_TIMEOUT = 5
86
87
88class FuchsiaDeviceError(signals.ControllerError):
89    pass
90
91
92def create(configs):
93    if not configs:
94        raise FuchsiaDeviceError(FUCHSIA_DEVICE_EMPTY_CONFIG_MSG)
95    elif not isinstance(configs, list):
96        raise FuchsiaDeviceError(FUCHSIA_DEVICE_NOT_LIST_CONFIG_MSG)
97    for index, config in enumerate(configs):
98        if isinstance(config, str):
99            configs[index] = {"ip": config}
100        elif not isinstance(config, dict):
101            raise FuchsiaDeviceError(FUCHSIA_DEVICE_INVALID_CONFIG %
102                                     (index, configs))
103    return get_instances(configs)
104
105
106def destroy(fds):
107    for fd in fds:
108        fd.clean_up()
109        del fd
110
111
112def get_info(fds):
113    """Get information on a list of FuchsiaDevice objects.
114
115    Args:
116        fds: A list of FuchsiaDevice objects.
117
118    Returns:
119        A list of dict, each representing info for FuchsiaDevice objects.
120    """
121    device_info = []
122    for fd in fds:
123        info = {"ip": fd.ip}
124        device_info.append(info)
125    return device_info
126
127
128def get_instances(fds_conf_data):
129    """Create FuchsiaDevice instances from a list of Fuchsia ips.
130
131    Args:
132        fds_conf_data: A list of dicts that contain Fuchsia device info.
133
134    Returns:
135        A list of FuchsiaDevice objects.
136    """
137
138    return [FuchsiaDevice(fd_conf_data) for fd_conf_data in fds_conf_data]
139
140
141class FuchsiaDevice:
142    """Class representing a Fuchsia device.
143
144    Each object of this class represents one Fuchsia device in ACTS.
145
146    Attributes:
147        address: The full address to contact the Fuchsia device at
148        log: A logger object.
149        port: The TCP port number of the Fuchsia device.
150    """
151    def __init__(self, fd_conf_data):
152        """
153        Args:
154            fd_conf_data: A dict of a fuchsia device configuration data
155                Required keys:
156                    ip: IP address of fuchsia device
157                optional key:
158                    port: Port for the sl4f web server on the fuchsia device
159                        (Default: 80)
160                    ssh_config: Location of the ssh_config file to connect to
161                        the fuchsia device
162                        (Default: None)
163        """
164        if "ip" not in fd_conf_data:
165            raise FuchsiaDeviceError(FUCHSIA_DEVICE_NO_IP_MSG)
166        self.ip = fd_conf_data["ip"]
167        self.port = fd_conf_data.get("port", 80)
168        self.ssh_config = fd_conf_data.get("ssh_config", None)
169        self.ssh_username = fd_conf_data.get("ssh_username",
170                                             FUCHSIA_SSH_USERNAME)
171        self._persistent_ssh_conn = None
172
173        self.log = acts_logger.create_tagged_trace_logger(
174            "FuchsiaDevice | %s" % self.ip)
175
176        if utils.is_valid_ipv4_address(self.ip):
177            self.address = "http://{}:{}".format(self.ip, self.port)
178        elif utils.is_valid_ipv6_address(self.ip):
179            self.address = "http://[{}]:{}".format(self.ip, self.port)
180        else:
181            raise ValueError('Invalid IP: %s' % self.ip)
182
183        self.init_address = self.address + "/init"
184        self.cleanup_address = self.address + "/cleanup"
185        self.print_address = self.address + "/print_clients"
186        self.ping_rtt_match = re.compile(r'RTT Min/Max/Avg '
187                                         r'= \[ (.*?) / (.*?) / (.*?) \] ms')
188
189        # TODO(): Come up with better client numbering system
190        self.client_id = "FuchsiaClient" + str(random.randint(0, 1000000))
191        self.test_counter = 0
192        self.serial = re.sub('[.:%]', '_', self.ip)
193        log_path_base = getattr(logging, 'log_path', '/tmp/logs')
194        self.log_path = os.path.join(log_path_base,
195                                     'FuchsiaDevice%s' % self.serial)
196        self.fuchsia_log_file_path = os.path.join(
197            self.log_path, "fuchsialog_%s_debug.txt" % self.serial)
198        self.log_process = None
199
200        # Grab commands from FuchsiaAvdtpLib
201        self.avdtp_lib = FuchsiaAvdtpLib(self.address, self.test_counter,
202                                         self.client_id)
203        # Grab commands from FuchsiaBleLib
204        self.ble_lib = FuchsiaBleLib(self.address, self.test_counter,
205                                     self.client_id)
206        # Grab commands from FuchsiaBtcLib
207        self.btc_lib = FuchsiaBtcLib(self.address, self.test_counter,
208                                     self.client_id)
209        # Grab commands from FuchsiaGattcLib
210        self.gattc_lib = FuchsiaGattcLib(self.address, self.test_counter,
211                                         self.client_id)
212        # Grab commands from FuchsiaGattsLib
213        self.gatts_lib = FuchsiaGattsLib(self.address, self.test_counter,
214                                         self.client_id)
215
216        # Grab commands from FuchsiaHwinfoLib
217        self.hwinfo_lib = FuchsiaHwinfoLib(self.address, self.test_counter,
218                                           self.client_id)
219
220        # Grab commands from FuchsiaLoggingLib
221        self.logging_lib = FuchsiaLoggingLib(self.address, self.test_counter,
222                                             self.client_id)
223
224        # Grab commands from FuchsiaNetstackLib
225        self.netstack_lib = FuchsiaNetstackLib(self.address, self.test_counter,
226                                               self.client_id)
227
228        # Grab commands from FuchsiaProfileServerLib
229        self.sdp_lib = FuchsiaProfileServerLib(self.address, self.test_counter,
230                                               self.client_id)
231
232        # Grab commands from FuchsiaWlanLib
233        self.wlan_lib = FuchsiaWlanLib(self.address, self.test_counter,
234                                       self.client_id)
235        self.skip_sl4f = False
236        # Start sl4f on device
237        self.start_services(skip_sl4f=self.skip_sl4f)
238        # Init server
239        self.init_server_connection()
240
241    @backoff.on_exception(
242        backoff.constant,
243        (ConnectionRefusedError, requests.exceptions.ConnectionError),
244        interval=1.5,
245        max_tries=4)
246    def init_server_connection(self):
247        """Initializes HTTP connection with SL4F server."""
248        self.log.debug("Initialziing server connection")
249        init_data = json.dumps({
250            "jsonrpc": "2.0",
251            "id": self.build_id(self.test_counter),
252            "method": "sl4f.sl4f_init",
253            "params": {
254                "client_id": self.client_id
255            }
256        })
257
258        requests.get(url=self.init_address, data=init_data)
259        self.test_counter += 1
260
261    def build_id(self, test_id):
262        """Concatenates client_id and test_id to form a command_id
263
264        Args:
265            test_id: string, unique identifier of test command
266        """
267        return self.client_id + "." + str(test_id)
268
269    def send_command_sl4f(self, test_id, test_cmd, test_args):
270        """Builds and sends a JSON command to SL4F server.
271
272        Args:
273            test_id: string, unique identifier of test command.
274            test_cmd: string, sl4f method name of command.
275            test_args: dictionary, arguments required to execute test_cmd.
276
277        Returns:
278            Dictionary, Result of sl4f command executed.
279        """
280        test_data = json.dumps({
281            "jsonrpc": "2.0",
282            "id": self.build_id(self.test_counter),
283            "method": test_cmd,
284            "params": test_args
285        })
286        return requests.get(url=self.address, data=test_data).json()
287
288    def reboot(self, timeout=60):
289        """Reboot a Fuchsia device and restablish all the services after reboot
290
291        Disables the logging when sending the reboot command
292        because the ssh session does not disconnect cleanly and therefore
293        would throw an error.  This is expected and thus the error logging
294        is disabled for this call.
295
296        Args:
297            timeout: How long to wait for the device to reboot.
298        """
299        timeout_flag = None
300        os_type = platform.system()
301        if os_type == 'Darwin':
302            timeout_flag = '-t'
303        elif os_type == 'Linux':
304            timeout_flag = '-W'
305        else:
306            raise ValueError(
307                'Invalid OS.  Only Linux and MacOS are supported.')
308        ping_command = ['ping', '%s' % timeout_flag, '1', '-c', '1', self.ip]
309        self.clean_up()
310        self.log.info('Rebooting FuchsiaDevice %s' % self.ip)
311        # Disables the logging when sending the reboot command
312        # because the ssh session does not disconnect cleanly and therefore
313        # would throw an error.  This is expected and thus the error logging
314        # is disabled for this call to not confuse the user.
315        with utils.SuppressLogOutput():
316            self.send_command_ssh('dm reboot',
317                                  timeout=FUCHSIA_RECONNECT_AFTER_REBOOT_TIME,
318                                  skip_status_code_check=True)
319        initial_ping_start_time = time.time()
320        self.log.info('Waiting for FuchsiaDevice %s to come back up.' %
321                      self.ip)
322        self.log.debug('Waiting for FuchsiaDevice %s to stop responding'
323                       ' to pings.' % self.ip)
324        while True:
325            initial_ping_status_code = subprocess.call(
326                ping_command,
327                stdout=subprocess.DEVNULL,
328                stderr=subprocess.STDOUT)
329            if initial_ping_status_code != 1:
330                break
331            else:
332                initial_ping_elapsed_time = (time.time() -
333                                             initial_ping_start_time)
334                if initial_ping_elapsed_time > timeout:
335                    try:
336                        uptime = (int(
337                            self.send_command_ssh(
338                                'clock --monotonic',
339                                timeout=FUCHSIA_RECONNECT_AFTER_REBOOT_TIME).
340                            stdout) / FUCHSIA_TIME_IN_NANOSECONDS)
341                    except Exception as e:
342                        self.log.info('Unable to retrieve uptime from device.')
343                    # Device failed to restart within the specified period.
344                    # Restart the services so other tests can continue.
345                    self.start_services()
346                    self.init_server_connection()
347                    raise TimeoutError(
348                        'Waited %s seconds, and FuchsiaDevice %s'
349                        ' never stopped responding to pings.'
350                        ' Uptime reported as %s' %
351                        (initial_ping_elapsed_time, self.ip, str(uptime)))
352
353        start_time = time.time()
354        self.log.debug('Waiting for FuchsiaDevice %s to start responding '
355                       'to pings.' % self.ip)
356        while True:
357            ping_status_code = subprocess.call(ping_command,
358                                               stdout=subprocess.DEVNULL,
359                                               stderr=subprocess.STDOUT)
360            if ping_status_code == 0:
361                break
362            elapsed_time = time.time() - start_time
363            if elapsed_time > timeout:
364                raise TimeoutError('Waited %s seconds, and FuchsiaDevice %s'
365                                   'did not repond to a ping.' %
366                                   (elapsed_time, self.ip))
367        self.log.debug('Received a ping back in %s seconds.' %
368                       str(time.time() - start_time))
369        # Wait 5 seconds after receiving a ping packet to just to let
370        # the OS get everything up and running.
371        time.sleep(10)
372        # Start sl4f on device
373        self.start_services()
374        # Init server
375        self.init_server_connection()
376
377    def send_command_ssh(self,
378                         test_cmd,
379                         connect_timeout=30,
380                         timeout=3600,
381                         skip_status_code_check=False):
382        """Sends an SSH command to a Fuchsia device
383
384        Args:
385            test_cmd: string, command to send to Fuchsia device over SSH.
386            connect_timeout: Timeout to wait for connecting via SSH.
387            timeout: Timeout to wait for a command to complete.
388            skip_status_code_check: Whether to check for the status code.
389
390        Returns:
391            A SshResults object containing the results of the ssh command.
392        """
393        command_result = False
394        ssh_conn = None
395        if not self.ssh_config:
396            self.log.warning(FUCHSIA_SSH_CONFIG_NOT_DEFINED)
397        else:
398            try:
399                ssh_conn = create_ssh_connection(
400                    self.ip,
401                    self.ssh_username,
402                    self.ssh_config,
403                    connect_timeout=connect_timeout)
404                cmd_result_stdin, cmd_result_stdout, cmd_result_stderr = (
405                    ssh_conn.exec_command(test_cmd, timeout=timeout))
406                if not skip_status_code_check:
407                    command_result = SshResults(cmd_result_stdin,
408                                                cmd_result_stdout,
409                                                cmd_result_stderr,
410                                                cmd_result_stdout.channel)
411            except Exception as e:
412                self.log.warning("Problem running ssh command: %s"
413                                 "\n Exception: %s" % (test_cmd, e))
414                return e
415            finally:
416                if ssh_conn is not None:
417                    ssh_conn.close()
418        return command_result
419
420    def ping(self, dest_ip, count=3, interval=1000, timeout=1000, size=25):
421        """Pings from a Fuchsia device to an IPv4 address or hostname
422
423        Args:
424            dest_ip: (str) The ip or hostname to ping.
425            count: (int) How many icmp packets to send.
426            interval: (int) How long to wait between pings (ms)
427            timeout: (int) How long to wait before having the icmp packet
428                timeout (ms).
429            size: (int) Size of the icmp packet.
430
431        Returns:
432            A dictionary for the results of the ping.  The dictionary contains
433            the following items:
434                status: Whether the ping was successful.
435                rtt_min: The minimum round trip time of the ping.
436                rtt_max: The minimum round trip time of the ping.
437                rtt_avg: The avg round trip time of the ping.
438                stdout: The standard out of the ping command.
439                stderr: The standard error of the ping command.
440        """
441        rtt_min = None
442        rtt_max = None
443        rtt_avg = None
444        self.log.debug("Pinging %s..." % dest_ip)
445        ping_result = self.send_command_ssh(
446            'ping -c %s -i %s -t %s -s %s %s' %
447            (count, interval, timeout, size, dest_ip))
448        if isinstance(ping_result, Error):
449            ping_result = ping_result.result
450
451        if ping_result.stderr:
452            status = False
453        else:
454            status = True
455            rtt_line = ping_result.stdout.split('\n')[:-1]
456            rtt_line = rtt_line[-1]
457            rtt_stats = re.search(self.ping_rtt_match, rtt_line)
458            rtt_min = rtt_stats.group(1)
459            rtt_max = rtt_stats.group(2)
460            rtt_avg = rtt_stats.group(3)
461        return {
462            'status': status,
463            'rtt_min': rtt_min,
464            'rtt_max': rtt_max,
465            'rtt_avg': rtt_avg,
466            'stdout': ping_result.stdout,
467            'stderr': ping_result.stderr
468        }
469
470    def print_clients(self):
471        """Gets connected clients from SL4F server"""
472        self.log.debug("Request to print clients")
473        print_id = self.build_id(self.test_counter)
474        print_args = {}
475        print_method = "sl4f.sl4f_print_clients"
476        data = json.dumps({
477            "jsonrpc": "2.0",
478            "id": print_id,
479            "method": print_method,
480            "params": print_args
481        })
482
483        r = requests.get(url=self.print_address, data=data).json()
484        self.test_counter += 1
485
486        return r
487
488    def clean_up(self):
489        """Cleans up the FuchsiaDevice object and releases any resources it
490        claimed.
491        """
492        cleanup_id = self.build_id(self.test_counter)
493        cleanup_args = {}
494        cleanup_method = "sl4f.sl4f_cleanup"
495        data = json.dumps({
496            "jsonrpc": "2.0",
497            "id": cleanup_id,
498            "method": cleanup_method,
499            "params": cleanup_args
500        })
501
502        try:
503            response = requests.get(url=self.cleanup_address, data=data).json()
504            self.log.debug(response)
505        except Exception as err:
506            self.log.exception("Cleanup request failed with %s:" % err)
507        finally:
508            self.test_counter += 1
509            self.stop_services()
510
511    def check_process_state(self, process_name):
512        """Checks the state of a process on the Fuchsia device
513
514        Returns:
515            True if the process_name is running
516            False if process_name is not running
517        """
518        ps_cmd = self.send_command_ssh("ps")
519        return process_name in ps_cmd.stdout
520
521    def check_process_with_expectation(self, process_name, expectation=None):
522        """Checks the state of a process on the Fuchsia device and returns
523        true or false depending the stated expectation
524
525        Args:
526            process_name: The name of the process to check for.
527            expectation: The state expectation of state of process
528        Returns:
529            True if the state of the process matches the expectation
530            False if the state of the process does not match the expectation
531        """
532        process_state = self.check_process_state(process_name)
533        if expectation in DAEMON_ACTIVATED_STATES:
534            return process_state
535        elif expectation in DAEMON_DEACTIVATED_STATES:
536            return not process_state
537        else:
538            raise ValueError("Invalid expectation value (%s). abort!" %
539                             expectation)
540
541    def control_daemon(self, process_name, action):
542        """Starts or stops a process on a Fuchsia device
543
544        Args:
545            process_name: the name of the process to start or stop
546            action: specify whether to start or stop a process
547        """
548        if not process_name[-4:] == '.cmx':
549            process_name = '%s.cmx' % process_name
550        unable_to_connect_msg = None
551        process_state = False
552        try:
553            if not self._persistent_ssh_conn:
554                self._persistent_ssh_conn = (create_ssh_connection(
555                    self.ip, self.ssh_username, self.ssh_config))
556            self._persistent_ssh_conn.exec_command(
557                "killall %s" % process_name, timeout=CHANNEL_OPEN_TIMEOUT)
558            # This command will effectively stop the process but should
559            # be used as a cleanup before starting a process.  It is a bit
560            # confusing to have the msg saying "attempting to stop
561            # the process" after the command already tried but since both start
562            # and stop need to run this command, this is the best place
563            # for the command.
564            if action in DAEMON_ACTIVATED_STATES:
565                self.log.debug("Attempting to start Fuchsia "
566                               "devices services.")
567                self._persistent_ssh_conn.exec_command(
568                    "run fuchsia-pkg://fuchsia.com/%s#meta/%s &" %
569                    (process_name[:-4], process_name))
570                process_initial_msg = (
571                    "%s has not started yet. Waiting %i second and "
572                    "checking again." %
573                    (process_name, DAEMON_INIT_TIMEOUT_SEC))
574                process_timeout_msg = ("Timed out waiting for %s to start." %
575                                       process_name)
576                unable_to_connect_msg = ("Unable to start %s no Fuchsia "
577                                         "device via SSH. %s may not "
578                                         "be started." %
579                                         (process_name, process_name))
580            elif action in DAEMON_DEACTIVATED_STATES:
581                process_initial_msg = ("%s is running. Waiting %i second and "
582                                       "checking again." %
583                                       (process_name, DAEMON_INIT_TIMEOUT_SEC))
584                process_timeout_msg = ("Timed out waiting trying to kill %s." %
585                                       process_name)
586                unable_to_connect_msg = ("Unable to stop %s on Fuchsia "
587                                         "device via SSH. %s may "
588                                         "still be running." %
589                                         (process_name, process_name))
590            else:
591                raise FuchsiaDeviceError(FUCHSIA_INVALID_CONTROL_STATE %
592                                         action)
593            timeout_counter = 0
594            while not process_state:
595                self.log.info(process_initial_msg)
596                time.sleep(DAEMON_INIT_TIMEOUT_SEC)
597                timeout_counter += 1
598                process_state = (self.check_process_with_expectation(
599                    process_name, expectation=action))
600                if timeout_counter == (DAEMON_INIT_TIMEOUT_SEC * 3):
601                    self.log.info(process_timeout_msg)
602                    break
603            if not process_state:
604                raise FuchsiaDeviceError(FUCHSIA_COULD_NOT_GET_DESIRED_STATE %
605                                         (action, process_name))
606        except Exception as e:
607            self.log.info(unable_to_connect_msg)
608            raise e
609        finally:
610            if action == 'stop' and (process_name == 'sl4f'
611                                     or process_name == 'sl4f.cmx'):
612                self._persistent_ssh_conn.close()
613                self._persistent_ssh_conn = None
614
615    def check_connect_response(self, connect_response):
616        if connect_response.get("error") is None:
617            # Checks the response from SL4F and if there is no error, check
618            # the result.
619            connection_result = connect_response.get("result")
620            if not connection_result:
621                # Ideally the error would be present but just outputting a log
622                # message until available.
623                self.log.debug("Connect call failed, aborting!")
624                return False
625            else:
626                # Returns True if connection was successful.
627                return True
628        else:
629            # the response indicates an error - log and raise failure
630            self.log.debug("Aborting! - Connect call failed with error: %s" %
631                           connect_response.get("error"))
632            return False
633
634    def check_disconnect_response(self, disconnect_response):
635        if disconnect_response.get("error") is None:
636            # Returns True if disconnect was successful.
637            return True
638        else:
639            # the response indicates an error - log and raise failure
640            self.log.debug("Disconnect call failed with error: %s" %
641                           disconnect_response.get("error"))
642            return False
643
644    @backoff.on_exception(backoff.constant,
645                          (FuchsiaSyslogError, socket.timeout),
646                          interval=1.5,
647                          max_tries=4)
648    def start_services(self, skip_sl4f=False):
649        """Starts long running services on the Fuchsia device.
650
651        1. Start SL4F if not skipped.
652
653        Args:
654            skip_sl4f: Does not attempt to start SL4F if True.
655        """
656        self.log.debug("Attempting to start Fuchsia device services on %s." %
657                       self.ip)
658        if self.ssh_config:
659            self.log_process = start_syslog(self.serial, self.log_path,
660                                            self.ip, self.ssh_username,
661                                            self.ssh_config)
662
663            if ENABLE_LOG_LISTENER:
664                self.log_process.start()
665
666            if not skip_sl4f:
667                self.control_daemon("sl4f.cmx", "start")
668
669    def stop_services(self):
670        """Stops long running services on the fuchsia device.
671
672        Terminate sl4f sessions if exist.
673        """
674        self.log.debug("Attempting to stop Fuchsia device services on %s." %
675                       self.ip)
676        if self.ssh_config:
677            try:
678                self.control_daemon("sl4f.cmx", "stop")
679            except Exception as err:
680                self.log.exception("Failed to stop sl4f.cmx with: %s" % err)
681            if self.log_process:
682                if ENABLE_LOG_LISTENER:
683                    self.log_process.stop()
684
685    def load_config(self, config):
686        pass
687
688    def take_bug_report(self,
689                        test_name,
690                        begin_time,
691                        additional_log_objects=None):
692        """Takes a bug report on the device and stores it in a file.
693
694        Args:
695            test_name: Name of the test case that triggered this bug report.
696            begin_time: Epoch time when the test started.
697            additional_log_objects: A list of additional objects in Fuchsia to
698                query in the bug report.  Must be in the following format:
699                /hub/c/scenic.cmx/[0-9]*/out/objects
700        """
701        if not additional_log_objects:
702            additional_log_objects = []
703        log_items = []
704        matching_log_items = FUCHSIA_DEFAULT_LOG_ITEMS
705        for additional_log_object in additional_log_objects:
706            if additional_log_object not in matching_log_items:
707                matching_log_items.append(additional_log_object)
708        br_path = context.get_current_context().get_full_output_path()
709        os.makedirs(br_path, exist_ok=True)
710        time_stamp = acts_logger.normalize_log_line_timestamp(
711            acts_logger.epoch_to_log_line_timestamp(begin_time))
712        out_name = "FuchsiaDevice%s_%s" % (
713            self.serial, time_stamp.replace(" ", "_").replace(":", "-"))
714        out_name = "%s.txt" % out_name
715        full_out_path = os.path.join(br_path, out_name)
716        self.log.info("Taking bugreport for %s on FuchsiaDevice%s." %
717                      (test_name, self.serial))
718        system_objects = self.send_command_ssh('iquery --find /hub').stdout
719        system_objects = system_objects.split()
720
721        for matching_log_item in matching_log_items:
722            for system_object in system_objects:
723                if re.match(matching_log_item, system_object):
724                    log_items.append(system_object)
725
726        log_command = '%s %s' % (FUCHSIA_DEFAULT_LOG_CMD, ' '.join(log_items))
727        bug_report_data = self.send_command_ssh(log_command).stdout
728
729        bug_report_file = open(full_out_path, 'w')
730        bug_report_file.write(bug_report_data)
731        bug_report_file.close()
732
733    def take_bt_snoop_log(self, custom_name=None):
734        """Takes a the bt-snoop log from the device and stores it in a file
735        in a pcap format.
736        """
737        bt_snoop_path = context.get_current_context().get_full_output_path()
738        time_stamp = acts_logger.normalize_log_line_timestamp(
739            acts_logger.epoch_to_log_line_timestamp(time.time()))
740        out_name = "FuchsiaDevice%s_%s" % (
741            self.serial, time_stamp.replace(" ", "_").replace(":", "-"))
742        out_name = "%s.pcap" % out_name
743        if custom_name:
744            out_name = "%s.pcap" % custom_name
745        else:
746            out_name = "%s.pcap" % out_name
747        full_out_path = os.path.join(bt_snoop_path, out_name)
748        bt_snoop_data = self.send_command_ssh('bt-snoop-cli -d -f pcap').stdout
749        bt_snoop_file = open(full_out_path, 'w')
750        bt_snoop_file.write(bt_snoop_data)
751        bt_snoop_file.close()
752
753
754class FuchsiaDeviceLoggerAdapter(logging.LoggerAdapter):
755    def process(self, msg, kwargs):
756        msg = "[FuchsiaDevice|%s] %s" % (self.extra["ip"], msg)
757        return msg, kwargs
758