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 import pdu
35
36from acts.controllers.fuchsia_lib.backlight_lib import FuchsiaBacklightLib
37from acts.controllers.fuchsia_lib.bt.avdtp_lib import FuchsiaAvdtpLib
38from acts.controllers.fuchsia_lib.light_lib import FuchsiaLightLib
39
40from acts.controllers.fuchsia_lib.bt.ble_lib import FuchsiaBleLib
41from acts.controllers.fuchsia_lib.bt.btc_lib import FuchsiaBtcLib
42from acts.controllers.fuchsia_lib.bt.gattc_lib import FuchsiaGattcLib
43from acts.controllers.fuchsia_lib.bt.gatts_lib import FuchsiaGattsLib
44from acts.controllers.fuchsia_lib.bt.sdp_lib import FuchsiaProfileServerLib
45from acts.controllers.fuchsia_lib.gpio_lib import FuchsiaGpioLib
46from acts.controllers.fuchsia_lib.hardware_power_statecontrol_lib import FuchsiaHardwarePowerStatecontrolLib
47from acts.controllers.fuchsia_lib.hwinfo_lib import FuchsiaHwinfoLib
48from acts.controllers.fuchsia_lib.i2c_lib import FuchsiaI2cLib
49from acts.controllers.fuchsia_lib.input_report_lib import FuchsiaInputReportLib
50from acts.controllers.fuchsia_lib.kernel_lib import FuchsiaKernelLib
51from acts.controllers.fuchsia_lib.location.regulatory_region_lib import FuchsiaRegulatoryRegionLib
52from acts.controllers.fuchsia_lib.logging_lib import FuchsiaLoggingLib
53from acts.controllers.fuchsia_lib.netstack.netstack_lib import FuchsiaNetstackLib
54from acts.controllers.fuchsia_lib.syslog_lib import FuchsiaSyslogError
55from acts.controllers.fuchsia_lib.syslog_lib import start_syslog
56from acts.controllers.fuchsia_lib.sysinfo_lib import FuchsiaSysInfoLib
57from acts.controllers.fuchsia_lib.utils_lib import create_ssh_connection
58from acts.controllers.fuchsia_lib.utils_lib import SshResults
59from acts.controllers.fuchsia_lib.wlan_deprecated_configuration_lib import FuchsiaWlanDeprecatedConfigurationLib
60from acts.controllers.fuchsia_lib.wlan_lib import FuchsiaWlanLib
61from acts.controllers.fuchsia_lib.wlan_ap_policy_lib import FuchsiaWlanApPolicyLib
62from acts.controllers.fuchsia_lib.wlan_policy_lib import FuchsiaWlanPolicyLib
63from acts.libs.proc.job import Error
64
65MOBLY_CONTROLLER_CONFIG_NAME = "FuchsiaDevice"
66ACTS_CONTROLLER_REFERENCE_NAME = "fuchsia_devices"
67
68FUCHSIA_DEVICE_EMPTY_CONFIG_MSG = "Configuration is empty, abort!"
69FUCHSIA_DEVICE_NOT_LIST_CONFIG_MSG = "Configuration should be a list, abort!"
70FUCHSIA_DEVICE_INVALID_CONFIG = ("Fuchsia device config must be either a str "
71                                 "or dict. abort! Invalid element %i in %r")
72FUCHSIA_DEVICE_NO_IP_MSG = "No IP address specified, abort!"
73FUCHSIA_COULD_NOT_GET_DESIRED_STATE = "Could not %s %s."
74FUCHSIA_INVALID_CONTROL_STATE = "Invalid control state (%s). abort!"
75FUCHSIA_SSH_CONFIG_NOT_DEFINED = ("Cannot send ssh commands since the "
76                                  "ssh_config was not specified in the Fuchsia"
77                                  "device config.")
78
79FUCHSIA_SSH_USERNAME = "fuchsia"
80FUCHSIA_TIME_IN_NANOSECONDS = 1000000000
81
82SL4F_APK_NAME = "com.googlecode.android_scripting"
83DAEMON_INIT_TIMEOUT_SEC = 1
84
85DAEMON_ACTIVATED_STATES = ["running", "start"]
86DAEMON_DEACTIVATED_STATES = ["stop", "stopped"]
87
88FUCHSIA_DEFAULT_LOG_CMD = 'iquery --absolute_paths --cat --format= --recursive'
89FUCHSIA_DEFAULT_LOG_ITEMS = [
90    '/hub/c/scenic.cmx/[0-9]*/out/objects',
91    '/hub/c/root_presenter.cmx/[0-9]*/out/objects',
92    '/hub/c/wlanstack2.cmx/[0-9]*/out/public',
93    '/hub/c/basemgr.cmx/[0-9]*/out/objects'
94]
95
96FUCHSIA_RECONNECT_AFTER_REBOOT_TIME = 5
97
98ENABLE_LOG_LISTENER = True
99
100CHANNEL_OPEN_TIMEOUT = 5
101
102FUCHSIA_GET_VERSION_CMD = 'cat /config/build-info/version'
103
104FUCHSIA_REBOOT_TYPE_SOFT = 'soft'
105FUCHSIA_REBOOT_TYPE_HARD = 'hard'
106
107
108class FuchsiaDeviceError(signals.ControllerError):
109    pass
110
111
112def create(configs):
113    if not configs:
114        raise FuchsiaDeviceError(FUCHSIA_DEVICE_EMPTY_CONFIG_MSG)
115    elif not isinstance(configs, list):
116        raise FuchsiaDeviceError(FUCHSIA_DEVICE_NOT_LIST_CONFIG_MSG)
117    for index, config in enumerate(configs):
118        if isinstance(config, str):
119            configs[index] = {"ip": config}
120        elif not isinstance(config, dict):
121            raise FuchsiaDeviceError(FUCHSIA_DEVICE_INVALID_CONFIG %
122                                     (index, configs))
123    return get_instances(configs)
124
125
126def destroy(fds):
127    for fd in fds:
128        fd.clean_up()
129        del fd
130
131
132def get_info(fds):
133    """Get information on a list of FuchsiaDevice objects.
134
135    Args:
136        fds: A list of FuchsiaDevice objects.
137
138    Returns:
139        A list of dict, each representing info for FuchsiaDevice objects.
140    """
141    device_info = []
142    for fd in fds:
143        info = {"ip": fd.ip}
144        device_info.append(info)
145    return device_info
146
147
148def get_instances(fds_conf_data):
149    """Create FuchsiaDevice instances from a list of Fuchsia ips.
150
151    Args:
152        fds_conf_data: A list of dicts that contain Fuchsia device info.
153
154    Returns:
155        A list of FuchsiaDevice objects.
156    """
157
158    return [FuchsiaDevice(fd_conf_data) for fd_conf_data in fds_conf_data]
159
160
161class FuchsiaDevice:
162    """Class representing a Fuchsia device.
163
164    Each object of this class represents one Fuchsia device in ACTS.
165
166    Attributes:
167        address: The full address to contact the Fuchsia device at
168        log: A logger object.
169        port: The TCP port number of the Fuchsia device.
170    """
171    def __init__(self, fd_conf_data):
172        """
173        Args:
174            fd_conf_data: A dict of a fuchsia device configuration data
175                Required keys:
176                    ip: IP address of fuchsia device
177                optional key:
178                    port: Port for the sl4f web server on the fuchsia device
179                        (Default: 80)
180                    ssh_config: Location of the ssh_config file to connect to
181                        the fuchsia device
182                        (Default: None)
183        """
184        self.conf_data = fd_conf_data
185        if "ip" not in fd_conf_data:
186            raise FuchsiaDeviceError(FUCHSIA_DEVICE_NO_IP_MSG)
187        self.ip = fd_conf_data["ip"]
188        self.port = fd_conf_data.get("port", 80)
189        self.ssh_config = fd_conf_data.get("ssh_config", None)
190        self.ssh_username = fd_conf_data.get("ssh_username",
191                                             FUCHSIA_SSH_USERNAME)
192        self.hard_reboot_on_fail = fd_conf_data.get("hard_reboot_on_fail",
193                                                    False)
194        self.device_pdu_config = fd_conf_data.get("PduDevice", None)
195        self._persistent_ssh_conn = None
196
197        self.log = acts_logger.create_tagged_trace_logger(
198            "FuchsiaDevice | %s" % self.ip)
199
200        if utils.is_valid_ipv4_address(self.ip):
201            self.address = "http://{}:{}".format(self.ip, self.port)
202        elif utils.is_valid_ipv6_address(self.ip):
203            self.address = "http://[{}]:{}".format(self.ip, self.port)
204        else:
205            raise ValueError('Invalid IP: %s' % self.ip)
206
207        self.init_address = self.address + "/init"
208        self.cleanup_address = self.address + "/cleanup"
209        self.print_address = self.address + "/print_clients"
210        self.ping_rtt_match = re.compile(r'RTT Min/Max/Avg '
211                                         r'= \[ (.*?) / (.*?) / (.*?) \] ms')
212
213        # TODO(): Come up with better client numbering system
214        self.client_id = "FuchsiaClient" + str(random.randint(0, 1000000))
215        self.test_counter = 0
216        self.serial = re.sub('[.:%]', '_', self.ip)
217        log_path_base = getattr(logging, 'log_path', '/tmp/logs')
218        self.log_path = os.path.join(log_path_base,
219                                     'FuchsiaDevice%s' % self.serial)
220        self.fuchsia_log_file_path = os.path.join(
221            self.log_path, "fuchsialog_%s_debug.txt" % self.serial)
222        self.log_process = None
223
224        # Grab commands from FuchsiaAvdtpLib
225        self.avdtp_lib = FuchsiaAvdtpLib(self.address, self.test_counter,
226                                         self.client_id)
227
228        # Grab commands from FuchsiaLightLib
229        self.light_lib = FuchsiaLightLib(self.address, self.test_counter,
230                                         self.client_id)
231
232        # Grab commands from FuchsiaBacklightLib
233        self.backlight_lib = FuchsiaBacklightLib(self.address,
234                                                 self.test_counter,
235                                                 self.client_id)
236
237        # Grab commands from FuchsiaBleLib
238        self.ble_lib = FuchsiaBleLib(self.address, self.test_counter,
239                                     self.client_id)
240        # Grab commands from FuchsiaBtcLib
241        self.btc_lib = FuchsiaBtcLib(self.address, self.test_counter,
242                                     self.client_id)
243        # Grab commands from FuchsiaGattcLib
244        self.gattc_lib = FuchsiaGattcLib(self.address, self.test_counter,
245                                         self.client_id)
246        # Grab commands from FuchsiaGattsLib
247        self.gatts_lib = FuchsiaGattsLib(self.address, self.test_counter,
248                                         self.client_id)
249
250        # Grab commands from FuchsiaGpioLib
251        self.gpio_lib = FuchsiaGpioLib(self.address, self.test_counter,
252                                       self.client_id)
253
254        # Grab commands from FuchsiaHardwarePowerStatecontrolLib
255        self.hardware_power_statecontrol_lib = FuchsiaHardwarePowerStatecontrolLib(
256            self.address, self.test_counter, self.client_id)
257
258        # Grab commands from FuchsiaHwinfoLib
259        self.hwinfo_lib = FuchsiaHwinfoLib(self.address, self.test_counter,
260                                           self.client_id)
261
262        # Grab commands from FuchsiaI2cLib
263        self.i2c_lib = FuchsiaI2cLib(self.address, self.test_counter,
264                                     self.client_id)
265
266        # Grab commands from FuchsiaInputReportLib
267        self.input_report_lib = FuchsiaInputReportLib(self.address,
268                                                      self.test_counter,
269                                                      self.client_id)
270
271        # Grab commands from FuchsiaKernelLib
272        self.kernel_lib = FuchsiaKernelLib(self.address, self.test_counter,
273                                           self.client_id)
274
275        # Grab commands from FuchsiaLoggingLib
276        self.logging_lib = FuchsiaLoggingLib(self.address, self.test_counter,
277                                             self.client_id)
278
279        # Grab commands from FuchsiaNetstackLib
280        self.netstack_lib = FuchsiaNetstackLib(self.address, self.test_counter,
281                                               self.client_id)
282
283        # Grab commands from FuchsiaProfileServerLib
284        self.sdp_lib = FuchsiaProfileServerLib(self.address, self.test_counter,
285                                               self.client_id)
286
287        # Grab commands from FuchsiaRegulatoryRegionLib
288        self.regulatory_region_lib = FuchsiaRegulatoryRegionLib(
289            self.address, self.test_counter, self.client_id)
290
291        # Grab commands from FuchsiaSysInfoLib
292        self.sysinfo_lib = FuchsiaSysInfoLib(self.address, self.test_counter,
293                                             self.client_id)
294
295        # Grabs command from FuchsiaWlanDeprecatedConfigurationLib
296        self.wlan_deprecated_configuration_lib = FuchsiaWlanDeprecatedConfigurationLib(
297            self.address, self.test_counter, self.client_id)
298
299        # Grab commands from FuchsiaWlanLib
300        self.wlan_lib = FuchsiaWlanLib(self.address, self.test_counter,
301                                       self.client_id)
302
303        # Grab commands from FuchsiaWlanApPolicyLib
304        self.wlan_ap_policy_lib = FuchsiaWlanApPolicyLib(
305            self.address, self.test_counter, self.client_id)
306
307        # Grab commands from FuchsiaWlanPolicyLib
308        self.wlan_policy_lib = FuchsiaWlanPolicyLib(self.address,
309                                                    self.test_counter,
310                                                    self.client_id)
311
312        self.skip_sl4f = False
313        # Start sl4f on device
314        self.start_services(skip_sl4f=self.skip_sl4f)
315        # Init server
316        self.init_server_connection()
317
318    @backoff.on_exception(
319        backoff.constant,
320        (ConnectionRefusedError, requests.exceptions.ConnectionError),
321        interval=1.5,
322        max_tries=4)
323    def init_server_connection(self):
324        """Initializes HTTP connection with SL4F server."""
325        self.log.debug("Initialziing server connection")
326        init_data = json.dumps({
327            "jsonrpc": "2.0",
328            "id": self.build_id(self.test_counter),
329            "method": "sl4f.sl4f_init",
330            "params": {
331                "client_id": self.client_id
332            }
333        })
334
335        requests.get(url=self.init_address, data=init_data)
336        self.test_counter += 1
337
338    def build_id(self, test_id):
339        """Concatenates client_id and test_id to form a command_id
340
341        Args:
342            test_id: string, unique identifier of test command
343        """
344        return self.client_id + "." + str(test_id)
345
346    def verify_ping(self, timeout=30):
347        """Verify the fuchsia device can be pinged.
348
349        Args:
350            timeout: int, seconds to retry before raising an exception
351
352        Raise:
353            ConnecitonError, if device still can't be pinged after timeout.
354        """
355        end_time = time.time() + timeout
356        while time.time() < end_time:
357            if utils.is_pingable(self.ip):
358                break
359            else:
360                self.log.debug('Device is not pingable. Retrying in 1 second.')
361                time.sleep(1)
362        else:
363            raise ConnectionError('Device never came back online.')
364
365    def verify_ssh(self, timeout=30):
366        """Verify the fuchsia device can be reached via ssh.
367
368        In self.reboot, this function is used to verify SSH is up before
369        attempting to restart SL4F, as that has more risky thread implications
370        if it fails. Also, create_ssh_connection has a short backoff loop,
371        but was not intended for waiting for SSH to come up.
372
373        Args:
374            timeout: int, seconds to retry before raising an exception
375
376        Raise:
377            ConnecitonError, if device still can't reached after timeout.
378        """
379        end_time = time.time() + timeout
380        while time.time() < end_time:
381            try:
382                self.send_command_ssh('\n')
383            except Exception:
384                self.log.debug(
385                    'Could not SSH to device. Retrying in 1 second.')
386                time.sleep(1)
387            else:
388                break
389        else:
390            raise ConnectionError('Failed to connect to device via SSH.')
391
392    def reboot(self,
393               use_ssh=False,
394               unreachable_timeout=30,
395               ping_timeout=30,
396               ssh_timeout=30,
397               reboot_type=FUCHSIA_REBOOT_TYPE_SOFT,
398               testbed_pdus=None):
399        """Reboot a FuchsiaDevice.
400
401        Soft reboots the device, verifies it becomes unreachable, then verfifies
402        it comes back online. Reinitializes SL4F so the tests can continue.
403
404        Args:
405            use_ssh: bool, if True, use fuchsia shell command via ssh to reboot
406                instead of SL4F.
407            unreachable_timeout: int, time to wait for device to become
408                unreachable.
409            ping_timeout: int, time to wait for device to respond to pings.
410            ssh_timeout: int, time to wait for device to be reachable via ssh.
411            reboot_type: boolFUCHSIA_REBOOT_TYPE_SOFT or
412                FUCHSIA_REBOOT_TYPE_HARD
413            testbed_pdus: list, all testbed PDUs
414
415        Raises:
416            ConnectionError, if device fails to become unreachable, fails to
417                come back up, or if SL4F does not setup correctly.
418        """
419        # Call Reboot
420        if reboot_type == FUCHSIA_REBOOT_TYPE_SOFT:
421            if use_ssh:
422                self.log.info('Sending reboot command via SSH.')
423                with utils.SuppressLogOutput():
424                    self.clean_up()
425                    self.send_command_ssh(
426                        'dm reboot',
427                        timeout=FUCHSIA_RECONNECT_AFTER_REBOOT_TIME,
428                        skip_status_code_check=True)
429            else:
430                self.log.info('Initializing reboot of FuchsiaDevice (%s)'
431                              ' with SL4F.' % self.ip)
432                self.log.info('Calling SL4F reboot command.')
433                with utils.SuppressLogOutput():
434                    if self.log_process:
435                        self.log_process.stop()
436                    self.hardware_power_statecontrol_lib.suspendReboot(
437                        timeout=3)
438                    if self._persistent_ssh_conn:
439                        self._persistent_ssh_conn.close()
440                        self._persistent_ssh_conn = None
441        elif reboot_type == FUCHSIA_REBOOT_TYPE_HARD:
442            self.log.info('Power cycling FuchsiaDevice (%s)' % self.ip)
443            device_pdu, device_pdu_port = pdu.get_pdu_port_for_device(
444                self.device_pdu_config, testbed_pdus)
445            with utils.SuppressLogOutput():
446                if self.log_process:
447                    self.log_process.stop()
448                if self._persistent_ssh_conn:
449                    self._persistent_ssh_conn.close()
450                    self._persistent_ssh_conn = None
451            self.log.info('Killing power to FuchsiaDevice (%s)...' % self.ip)
452            device_pdu.off(str(device_pdu_port))
453
454        # Wait for unreachable
455        self.log.info('Verifying device is unreachable.')
456        timeout = time.time() + unreachable_timeout
457        while (time.time() < timeout):
458            if utils.is_pingable(self.ip):
459                self.log.debug('Device is still pingable. Retrying.')
460            else:
461                if reboot_type == FUCHSIA_REBOOT_TYPE_HARD:
462                    self.log.info('Restoring power to FuchsiaDevice (%s)...' %
463                                  self.ip)
464                    device_pdu.on(str(device_pdu_port))
465                break
466        else:
467            self.log.info('Device failed to go offline. Reintializing Sl4F.')
468            self.start_services()
469            self.init_server_connection()
470            raise ConnectionError('Device never went down.')
471        self.log.info('Device is unreachable as expected.')
472
473        if reboot_type == FUCHSIA_REBOOT_TYPE_HARD:
474            self.log.info('Restoring power to FuchsiaDevice (%s)...' % self.ip)
475            device_pdu.on(str(device_pdu_port))
476
477        self.log.info('Waiting for device to respond to pings.')
478        self.verify_ping(timeout=ping_timeout)
479        self.log.info('Device responded to pings.')
480
481        self.log.info('Waiting for device to allow ssh connection.')
482        self.verify_ssh(timeout=ssh_timeout)
483        self.log.info('Device now available via ssh.')
484
485        # Creating new log process, start it, start new persistent ssh session,
486        # start SL4F, and connect via SL4F
487        self.log.info(
488            'Restarting log process and reinitiating SL4F on FuchsiaDevice %s'
489            % self.ip)
490        self.log_process.start()
491        self.start_services()
492
493        # Verify SL4F is up.
494        self.log.info(
495            'Initiating connection to SL4F and verifying commands can run.')
496        try:
497            self.init_server_connection()
498            self.hwinfo_lib.getDeviceInfo()
499        except Exception as err:
500            raise ConnectionError(
501                'Failed to connect and run command via SL4F. Err: %s' % err)
502        self.log.info(
503            'Device has rebooted, SL4F is reconnected and functional.')
504
505    def send_command_ssh(self,
506                         test_cmd,
507                         connect_timeout=30,
508                         timeout=3600,
509                         skip_status_code_check=False):
510        """Sends an SSH command to a Fuchsia device
511
512        Args:
513            test_cmd: string, command to send to Fuchsia device over SSH.
514            connect_timeout: Timeout to wait for connecting via SSH.
515            timeout: Timeout to wait for a command to complete.
516            skip_status_code_check: Whether to check for the status code.
517
518        Returns:
519            A SshResults object containing the results of the ssh command.
520        """
521        command_result = False
522        ssh_conn = None
523        if not self.ssh_config:
524            self.log.warning(FUCHSIA_SSH_CONFIG_NOT_DEFINED)
525        else:
526            try:
527                ssh_conn = create_ssh_connection(
528                    self.ip,
529                    self.ssh_username,
530                    self.ssh_config,
531                    connect_timeout=connect_timeout)
532                cmd_result_stdin, cmd_result_stdout, cmd_result_stderr = (
533                    ssh_conn.exec_command(test_cmd, timeout=timeout))
534                if not skip_status_code_check:
535                    command_result = SshResults(cmd_result_stdin,
536                                                cmd_result_stdout,
537                                                cmd_result_stderr,
538                                                cmd_result_stdout.channel)
539            except Exception as e:
540                self.log.warning("Problem running ssh command: %s"
541                                 "\n Exception: %s" % (test_cmd, e))
542                return e
543            finally:
544                if ssh_conn is not None:
545                    ssh_conn.close()
546        return command_result
547
548    def ping(self, dest_ip, count=3, interval=1000, timeout=1000, size=25):
549        """Pings from a Fuchsia device to an IPv4 address or hostname
550
551        Args:
552            dest_ip: (str) The ip or hostname to ping.
553            count: (int) How many icmp packets to send.
554            interval: (int) How long to wait between pings (ms)
555            timeout: (int) How long to wait before having the icmp packet
556                timeout (ms).
557            size: (int) Size of the icmp packet.
558
559        Returns:
560            A dictionary for the results of the ping.  The dictionary contains
561            the following items:
562                status: Whether the ping was successful.
563                rtt_min: The minimum round trip time of the ping.
564                rtt_max: The minimum round trip time of the ping.
565                rtt_avg: The avg round trip time of the ping.
566                stdout: The standard out of the ping command.
567                stderr: The standard error of the ping command.
568        """
569        rtt_min = None
570        rtt_max = None
571        rtt_avg = None
572        self.log.debug("Pinging %s..." % dest_ip)
573        ping_result = self.send_command_ssh(
574            'ping -c %s -i %s -t %s -s %s %s' %
575            (count, interval, timeout, size, dest_ip))
576        if isinstance(ping_result, Error):
577            ping_result = ping_result.result
578
579        if ping_result.stderr:
580            status = False
581        else:
582            status = True
583            rtt_line = ping_result.stdout.split('\n')[:-1]
584            rtt_line = rtt_line[-1]
585            rtt_stats = re.search(self.ping_rtt_match, rtt_line)
586            rtt_min = rtt_stats.group(1)
587            rtt_max = rtt_stats.group(2)
588            rtt_avg = rtt_stats.group(3)
589        return {
590            'status': status,
591            'rtt_min': rtt_min,
592            'rtt_max': rtt_max,
593            'rtt_avg': rtt_avg,
594            'stdout': ping_result.stdout,
595            'stderr': ping_result.stderr
596        }
597
598    def print_clients(self):
599        """Gets connected clients from SL4F server"""
600        self.log.debug("Request to print clients")
601        print_id = self.build_id(self.test_counter)
602        print_args = {}
603        print_method = "sl4f.sl4f_print_clients"
604        data = json.dumps({
605            "jsonrpc": "2.0",
606            "id": print_id,
607            "method": print_method,
608            "params": print_args
609        })
610
611        r = requests.get(url=self.print_address, data=data).json()
612        self.test_counter += 1
613
614        return r
615
616    def clean_up(self):
617        """Cleans up the FuchsiaDevice object and releases any resources it
618        claimed.
619        """
620        cleanup_id = self.build_id(self.test_counter)
621        cleanup_args = {}
622        cleanup_method = "sl4f.sl4f_cleanup"
623        data = json.dumps({
624            "jsonrpc": "2.0",
625            "id": cleanup_id,
626            "method": cleanup_method,
627            "params": cleanup_args
628        })
629
630        try:
631            response = requests.get(url=self.cleanup_address, data=data).json()
632            self.log.debug(response)
633        except Exception as err:
634            self.log.exception("Cleanup request failed with %s:" % err)
635        finally:
636            self.test_counter += 1
637            self.stop_services()
638
639    def check_process_state(self, process_name):
640        """Checks the state of a process on the Fuchsia device
641
642        Returns:
643            True if the process_name is running
644            False if process_name is not running
645        """
646        ps_cmd = self.send_command_ssh("ps")
647        return process_name in ps_cmd.stdout
648
649    def check_process_with_expectation(self, process_name, expectation=None):
650        """Checks the state of a process on the Fuchsia device and returns
651        true or false depending the stated expectation
652
653        Args:
654            process_name: The name of the process to check for.
655            expectation: The state expectation of state of process
656        Returns:
657            True if the state of the process matches the expectation
658            False if the state of the process does not match the expectation
659        """
660        process_state = self.check_process_state(process_name)
661        if expectation in DAEMON_ACTIVATED_STATES:
662            return process_state
663        elif expectation in DAEMON_DEACTIVATED_STATES:
664            return not process_state
665        else:
666            raise ValueError("Invalid expectation value (%s). abort!" %
667                             expectation)
668
669    def control_daemon(self, process_name, action):
670        """Starts or stops a process on a Fuchsia device
671
672        Args:
673            process_name: the name of the process to start or stop
674            action: specify whether to start or stop a process
675        """
676        if not process_name[-4:] == '.cmx':
677            process_name = '%s.cmx' % process_name
678        unable_to_connect_msg = None
679        process_state = False
680        try:
681            if not self._persistent_ssh_conn:
682                self._persistent_ssh_conn = (create_ssh_connection(
683                    self.ip, self.ssh_username, self.ssh_config))
684            self._persistent_ssh_conn.exec_command(
685                "killall %s" % process_name, timeout=CHANNEL_OPEN_TIMEOUT)
686            # This command will effectively stop the process but should
687            # be used as a cleanup before starting a process.  It is a bit
688            # confusing to have the msg saying "attempting to stop
689            # the process" after the command already tried but since both start
690            # and stop need to run this command, this is the best place
691            # for the command.
692            if action in DAEMON_ACTIVATED_STATES:
693                self.log.debug("Attempting to start Fuchsia "
694                               "devices services.")
695                self._persistent_ssh_conn.exec_command(
696                    "run fuchsia-pkg://fuchsia.com/%s#meta/%s &" %
697                    (process_name[:-4], process_name))
698                process_initial_msg = (
699                    "%s has not started yet. Waiting %i second and "
700                    "checking again." %
701                    (process_name, DAEMON_INIT_TIMEOUT_SEC))
702                process_timeout_msg = ("Timed out waiting for %s to start." %
703                                       process_name)
704                unable_to_connect_msg = ("Unable to start %s no Fuchsia "
705                                         "device via SSH. %s may not "
706                                         "be started." %
707                                         (process_name, process_name))
708            elif action in DAEMON_DEACTIVATED_STATES:
709                process_initial_msg = ("%s is running. Waiting %i second and "
710                                       "checking again." %
711                                       (process_name, DAEMON_INIT_TIMEOUT_SEC))
712                process_timeout_msg = ("Timed out waiting trying to kill %s." %
713                                       process_name)
714                unable_to_connect_msg = ("Unable to stop %s on Fuchsia "
715                                         "device via SSH. %s may "
716                                         "still be running." %
717                                         (process_name, process_name))
718            else:
719                raise FuchsiaDeviceError(FUCHSIA_INVALID_CONTROL_STATE %
720                                         action)
721            timeout_counter = 0
722            while not process_state:
723                self.log.info(process_initial_msg)
724                time.sleep(DAEMON_INIT_TIMEOUT_SEC)
725                timeout_counter += 1
726                process_state = (self.check_process_with_expectation(
727                    process_name, expectation=action))
728                if timeout_counter == (DAEMON_INIT_TIMEOUT_SEC * 3):
729                    self.log.info(process_timeout_msg)
730                    break
731            if not process_state:
732                raise FuchsiaDeviceError(FUCHSIA_COULD_NOT_GET_DESIRED_STATE %
733                                         (action, process_name))
734        except Exception as e:
735            self.log.info(unable_to_connect_msg)
736            raise e
737        finally:
738            if action == 'stop' and (process_name == 'sl4f'
739                                     or process_name == 'sl4f.cmx'):
740                self._persistent_ssh_conn.close()
741                self._persistent_ssh_conn = None
742
743    def check_connect_response(self, connect_response):
744        if connect_response.get("error") is None:
745            # Checks the response from SL4F and if there is no error, check
746            # the result.
747            connection_result = connect_response.get("result")
748            if not connection_result:
749                # Ideally the error would be present but just outputting a log
750                # message until available.
751                self.log.debug("Connect call failed, aborting!")
752                return False
753            else:
754                # Returns True if connection was successful.
755                return True
756        else:
757            # the response indicates an error - log and raise failure
758            self.log.debug("Aborting! - Connect call failed with error: %s" %
759                           connect_response.get("error"))
760            return False
761
762    def check_disconnect_response(self, disconnect_response):
763        if disconnect_response.get("error") is None:
764            # Returns True if disconnect was successful.
765            return True
766        else:
767            # the response indicates an error - log and raise failure
768            self.log.debug("Disconnect call failed with error: %s" %
769                           disconnect_response.get("error"))
770            return False
771
772    @backoff.on_exception(backoff.constant,
773                          (FuchsiaSyslogError, socket.timeout),
774                          interval=1.5,
775                          max_tries=4)
776    def start_services(self, skip_sl4f=False):
777        """Starts long running services on the Fuchsia device.
778
779        1. Start SL4F if not skipped.
780
781        Args:
782            skip_sl4f: Does not attempt to start SL4F if True.
783        """
784        self.log.debug("Attempting to start Fuchsia device services on %s." %
785                       self.ip)
786        if self.ssh_config:
787            self.log_process = start_syslog(self.serial, self.log_path,
788                                            self.ip, self.ssh_username,
789                                            self.ssh_config)
790
791            if ENABLE_LOG_LISTENER:
792                try:
793                    self.log_process.start()
794                except FuchsiaSyslogError as e:
795                    # Before backing off and retrying, stop the syslog if it
796                    # failed to setup correctly, to prevent threading error when
797                    # retrying
798                    self.log_process.stop()
799                    raise
800
801            if not skip_sl4f:
802                self.control_daemon("sl4f.cmx", "start")
803
804            out_name = "fuchsia_device_%s_%s.txt" % (self.serial, 'fw_version')
805            full_out_path = os.path.join(self.log_path, out_name)
806            fuchsia_version = self.send_command_ssh(
807                FUCHSIA_GET_VERSION_CMD).stdout
808            fw_file = open(full_out_path, 'w')
809            fw_file.write('%s\n' % fuchsia_version)
810            fw_file.close()
811
812    def stop_services(self):
813        """Stops long running services on the fuchsia device.
814
815        Terminate sl4f sessions if exist.
816        """
817        self.log.debug("Attempting to stop Fuchsia device services on %s." %
818                       self.ip)
819        if self.ssh_config:
820            try:
821                self.control_daemon("sl4f.cmx", "stop")
822            except Exception as err:
823                self.log.exception("Failed to stop sl4f.cmx with: %s" % err)
824            if self.log_process:
825                if ENABLE_LOG_LISTENER:
826                    self.log_process.stop()
827
828    def reinitialize_services(self):
829        """Reinitialize long running services and establish connection to
830        SL4F."""
831        self.start_services()
832        self.init_server_connection()
833
834    def load_config(self, config):
835        pass
836
837    def take_bug_report(self,
838                        test_name,
839                        begin_time,
840                        additional_log_objects=None):
841        """Takes a bug report on the device and stores it in a file.
842
843        Args:
844            test_name: Name of the test case that triggered this bug report.
845            begin_time: Epoch time when the test started.
846            additional_log_objects: A list of additional objects in Fuchsia to
847                query in the bug report.  Must be in the following format:
848                /hub/c/scenic.cmx/[0-9]*/out/objects
849        """
850        if not additional_log_objects:
851            additional_log_objects = []
852        log_items = []
853        matching_log_items = FUCHSIA_DEFAULT_LOG_ITEMS
854        for additional_log_object in additional_log_objects:
855            if additional_log_object not in matching_log_items:
856                matching_log_items.append(additional_log_object)
857        br_path = context.get_current_context().get_full_output_path()
858        os.makedirs(br_path, exist_ok=True)
859        time_stamp = acts_logger.normalize_log_line_timestamp(
860            acts_logger.epoch_to_log_line_timestamp(begin_time))
861        out_name = "FuchsiaDevice%s_%s" % (
862            self.serial, time_stamp.replace(" ", "_").replace(":", "-"))
863        out_name = "%s.txt" % out_name
864        full_out_path = os.path.join(br_path, out_name)
865        self.log.info("Taking bugreport for %s on FuchsiaDevice%s." %
866                      (test_name, self.serial))
867        system_objects = self.send_command_ssh('iquery --find /hub').stdout
868        system_objects = system_objects.split()
869
870        for matching_log_item in matching_log_items:
871            for system_object in system_objects:
872                if re.match(matching_log_item, system_object):
873                    log_items.append(system_object)
874
875        log_command = '%s %s' % (FUCHSIA_DEFAULT_LOG_CMD, ' '.join(log_items))
876        bug_report_data = self.send_command_ssh(log_command).stdout
877
878        bug_report_file = open(full_out_path, 'w')
879        bug_report_file.write(bug_report_data)
880        bug_report_file.close()
881
882    def take_bt_snoop_log(self, custom_name=None):
883        """Takes a the bt-snoop log from the device and stores it in a file
884        in a pcap format.
885        """
886        bt_snoop_path = context.get_current_context().get_full_output_path()
887        time_stamp = acts_logger.normalize_log_line_timestamp(
888            acts_logger.epoch_to_log_line_timestamp(time.time()))
889        out_name = "FuchsiaDevice%s_%s" % (
890            self.serial, time_stamp.replace(" ", "_").replace(":", "-"))
891        out_name = "%s.pcap" % out_name
892        if custom_name:
893            out_name = "%s.pcap" % custom_name
894        else:
895            out_name = "%s.pcap" % out_name
896        full_out_path = os.path.join(bt_snoop_path, out_name)
897        bt_snoop_data = self.send_command_ssh('bt-snoop-cli -d -f pcap').stdout
898        bt_snoop_file = open(full_out_path, 'w')
899        bt_snoop_file.write(bt_snoop_data)
900        bt_snoop_file.close()
901
902
903class FuchsiaDeviceLoggerAdapter(logging.LoggerAdapter):
904    def process(self, msg, kwargs):
905        msg = "[FuchsiaDevice|%s] %s" % (self.extra["ip"], msg)
906        return msg, kwargs
907