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