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