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 json 18import socket 19 20from typing import Any, Mapping 21from urllib.parse import urlparse 22from urllib.request import Request, urlopen 23 24from acts import logger, utils 25from acts.libs.proc import job 26 27DEFAULT_SL4F_RESPONSE_TIMEOUT_SEC = 30 28 29 30class DeviceOffline(Exception): 31 """Exception if the device is no longer reachable via the network.""" 32 33 34class SL4FCommandFailed(Exception): 35 """A SL4F command to the server failed.""" 36 37 38class BaseLib(): 39 40 def __init__(self, addr: str, logger_tag: str) -> None: 41 self.address = addr 42 self.log = logger.create_tagged_trace_logger(f"SL4F | {self.address} | {logger_tag}") 43 44 def send_command( 45 self, 46 cmd: str, 47 args: Mapping[str, Any], 48 response_timeout: int = DEFAULT_SL4F_RESPONSE_TIMEOUT_SEC 49 ) -> Mapping[str, Any]: 50 """Builds and sends a JSON command to SL4F server. 51 52 Args: 53 cmd: SL4F method name of command. 54 args: Arguments required to execute cmd. 55 response_timeout: Seconds to wait for a response before 56 throwing an exception. 57 58 Returns: 59 Response from SL4F server. 60 61 Throws: 62 TimeoutError: The HTTP request timed out waiting for a response 63 """ 64 data = { 65 "jsonrpc": "2.0", 66 # id is required by the SL4F server to parse test_data but is not 67 # currently used. 68 "id": "", 69 "method": cmd, 70 "params": args 71 } 72 data_json = json.dumps(data).encode("utf-8") 73 req = Request(self.address, 74 data=data_json, 75 headers={ 76 "Content-Type": "application/json; charset=utf-8", 77 "Content-Length": len(data_json), 78 }) 79 80 self.log.debug(f'Sending request "{cmd}" with {args}') 81 try: 82 response = urlopen(req, timeout=response_timeout) 83 except (TimeoutError, socket.timeout) as e: 84 host = urlparse(self.address).hostname 85 if not utils.can_ping(job, host): 86 raise DeviceOffline( 87 f'FuchsiaDevice {host} is not reachable via the network.') 88 if type(e) == socket.timeout: 89 # socket.timeout was aliased to TimeoutError in Python 3.10. For 90 # older versions of Python, we need to cast to TimeoutError to 91 # provide a version-agnostic API. 92 raise TimeoutError("socket timeout") from e 93 raise e 94 95 response_body = response.read().decode("utf-8") 96 try: 97 response_json = json.loads(response_body) 98 self.log.debug(f'Received response for "{cmd}": {response_json}') 99 except json.JSONDecodeError as e: 100 raise SL4FCommandFailed(response_body) from e 101 102 # If the SL4F command fails it returns a str, without an 'error' field 103 # to get. 104 if not isinstance(response_json, dict): 105 raise SL4FCommandFailed(response_json) 106 107 return response_json 108