blob: e00acfd587bf93f18f1ffa75ce6cd0ec20bdf03c [file] [log] [blame]
#
# Copyright (C) 2016 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import json
import logging
import os
import socket
import time
from vts.proto import AndroidSystemControlMessage_pb2 as SysMsg_pb2
from vts.proto import ComponentSpecificationMessage_pb2 as CompSpecMsg_pb2
from vts.proto import VtsResourceControllerMessage_pb2 as ResControlMsg_pb2
from vts.runners.host import const
from vts.runners.host import errors
from vts.utils.python.mirror import mirror_object
from google.protobuf import text_format
TARGET_IP = os.environ.get("TARGET_IP", None)
TARGET_PORT = os.environ.get("TARGET_PORT", None)
_DEFAULT_SOCKET_TIMEOUT_SECS = 1800
_SOCKET_CONN_TIMEOUT_SECS = 60
_SOCKET_CONN_RETRY_NUMBER = 5
COMMAND_TYPE_NAME = {
1: "LIST_HALS",
2: "SET_HOST_INFO",
101: "CHECK_DRIVER_SERVICE",
102: "LAUNCH_DRIVER_SERVICE",
103: "VTS_AGENT_COMMAND_READ_SPECIFICATION",
201: "LIST_APIS",
202: "CALL_API",
203: "VTS_AGENT_COMMAND_GET_ATTRIBUTE",
301: "VTS_AGENT_COMMAND_EXECUTE_SHELL_COMMAND",
401: "VTS_FMQ_COMMAND",
402: "VTS_HIDL_MEMORY_COMMAND",
403: "VTS_HIDL_HANDLE_COMMAND"
}
class VtsTcpClient(object):
"""VTS TCP Client class.
Attribute:
NO_RESPONSE_MSG: string, error message when there is no response
from device.
FAIL_RESPONSE_MSG: string, error message when the device sends back
a failure message.
connection: a TCP socket instance.
channel: a file to write and read data.
error: string, ongoing tcp connection error. None means no error.
_mode: the connection mode (adb_forwarding or ssh_tunnel)
timeout: tcp connection timeout.
"""
NO_RESPONSE_MSG = "Framework error: TCP client did not receive response from device."
FAIL_RESPONSE_MSG = "Framework error: TCP client received unsuccessful response code."
def __init__(self,
mode="adb_forwarding",
timeout=_DEFAULT_SOCKET_TIMEOUT_SECS):
self.connection = None
self.channel = None
self._mode = mode
self.timeout = timeout
self.error = None
@property
def timeout(self):
"""Get TCP connection timeout.
This function assumes timeout property setter is in __init__before
any getter calls.
Returns:
int, timeout
"""
return self._timeout
@timeout.setter
def timeout(self, timeout):
"""Set TCP connection timeout.
Args:
timeout: int, TCP connection timeout in seconds.
"""
self._timeout = timeout
def Heal(self):
"""Performs a self healing.
Includes self diagnosis that looks for any framework errors.
Returns:
bool, True if everything is ok; False otherwise.
"""
res = self.error is None
if not res:
logging.error('Self diagnosis found problems in TCP client: %s', self.error)
return res
def Connect(self,
ip=TARGET_IP,
command_port=TARGET_PORT,
callback_port=None,
retry=_SOCKET_CONN_RETRY_NUMBER,
timeout=None):
"""Connects to a target device.
Args:
ip: string, the IP address of a target device.
command_port: int, the TCP port which can be used to connect to
a target device.
callback_port: int, the TCP port number of a host-side callback
server.
retry: int, the number of times to retry connecting before giving
up.
timeout: tcp connection timeout.
Returns:
True if success, False otherwise
Raises:
socket.error when the connection fails.
"""
if not command_port:
logging.error("ip %s, command_port %s, callback_port %s invalid",
ip, command_port, callback_port)
return False
for i in xrange(retry):
connection_timeout = self._timeout if timeout is None else timeout
try:
self.connection = socket.create_connection(
(ip, command_port), timeout=connection_timeout)
break
except socket.error as e:
# Wait a bit and retry.
logging.exception("Connect failed %s", e)
time.sleep(1)
if i + 1 == retry:
raise errors.VtsTcpClientCreationError(
"Couldn't connect to %s:%s" % (ip, command_port))
self.channel = self.connection.makefile(mode="brw")
if callback_port is not None:
self.SendCommand(
SysMsg_pb2.SET_HOST_INFO, callback_port=callback_port)
resp = self.RecvResponse()
if (resp.response_code != SysMsg_pb2.SUCCESS):
return False
return True
def Disconnect(self):
"""Disconnects from the target device.
TODO(yim): Send a msg to the target side to teardown handler session
and release memory before closing the socket.
"""
if self.connection is not None:
self.channel = None
self.connection.close()
self.connection = None
def ListHals(self, base_paths):
"""RPC to LIST_HALS."""
self.SendCommand(SysMsg_pb2.LIST_HALS, paths=base_paths)
resp = self.RecvResponse()
if (resp.response_code == SysMsg_pb2.SUCCESS):
return resp.file_names
return None
def CheckDriverService(self, service_name):
"""RPC to CHECK_DRIVER_SERVICE."""
self.SendCommand(
SysMsg_pb2.CHECK_DRIVER_SERVICE, service_name=service_name)
resp = self.RecvResponse()
return (resp.response_code == SysMsg_pb2.SUCCESS)
def LaunchDriverService(self,
driver_type,
service_name,
bits,
file_path=None,
target_class=None,
target_type=None,
target_version_major=None,
target_version_minor=None,
target_package=None,
target_component_name=None,
hw_binder_service_name=None,
is_test_hal=None):
"""RPC to LAUNCH_DRIVER_SERVICE.
Args:
driver_type: enum, type of the driver (shared lib, shell).
service_name: string, binder service name.
bits: int, whether a target driver binary is 64-bits or 32-bits.
file_path: string, the name of a target.
target_class: int, target class.
target_type: int, target type.
target_version_major: int, HAL major version, e.g. 1.0 -> 1.
target_version_minor: int, HAL minor version, e.g. 1.0 -> 0.
target_package: string, package name of a HIDL HAL.
target_component_name: string, name of a target component.
hw_binder_service_name: name of a HW Binder service to use.
is_test_hal: bool, whether the HAL service is a test HAL
(e.g. msgq).
Returns:
response code, -1 or 0 on failure, other values on success.
"""
logging.debug("service_name: %s", service_name)
logging.debug("file_path: %s", file_path)
logging.debug("bits: %s", bits)
logging.debug("driver_type: %s", driver_type)
self.SendCommand(
SysMsg_pb2.LAUNCH_DRIVER_SERVICE,
driver_type=driver_type,
service_name=service_name,
bits=bits,
file_path=file_path,
target_class=target_class,
target_type=target_type,
target_version_major=target_version_major,
target_version_minor=target_version_minor,
target_package=target_package,
target_component_name=target_component_name,
hw_binder_service_name=hw_binder_service_name,
is_test_hal=is_test_hal)
resp = self.RecvResponse()
logging.debug("resp for LAUNCH_DRIVER_SERVICE: %s", resp)
if driver_type == SysMsg_pb2.VTS_DRIVER_TYPE_HAL_HIDL \
or driver_type == SysMsg_pb2.VTS_DRIVER_TYPE_HAL_CONVENTIONAL \
or driver_type == SysMsg_pb2.VTS_DRIVER_TYPE_HAL_LEGACY:
if resp.response_code == SysMsg_pb2.SUCCESS:
return int(resp.result)
else:
return -1
else:
return (resp.response_code == SysMsg_pb2.SUCCESS)
def ListApis(self):
"""RPC to LIST_APIS."""
self.SendCommand(SysMsg_pb2.LIST_APIS)
resp = self.RecvResponse()
logging.debug("resp for LIST_APIS: %s", resp)
if (resp.response_code == SysMsg_pb2.SUCCESS):
return resp.spec
return None
def GetPythonDataOfVariableSpecMsg(self, var_spec_msg):
"""Returns the python native data structure for a given message.
Args:
var_spec_msg: VariableSpecificationMessage
Returns:
python native data structure (e.g., string, integer, list).
Raises:
VtsUnsupportedTypeError if unsupported type is specified.
VtsMalformedProtoStringError if StringDataValueMessage is
not populated.
"""
if var_spec_msg.type == CompSpecMsg_pb2.TYPE_SCALAR:
scalar_type = getattr(var_spec_msg, "scalar_type", "")
if scalar_type:
return getattr(var_spec_msg.scalar_value, scalar_type)
elif var_spec_msg.type == CompSpecMsg_pb2.TYPE_ENUM:
scalar_type = getattr(var_spec_msg, "scalar_type", "")
if scalar_type:
return getattr(var_spec_msg.scalar_value, scalar_type)
else:
return var_spec_msg.scalar_value.int32_t
elif var_spec_msg.type == CompSpecMsg_pb2.TYPE_STRING:
if hasattr(var_spec_msg, "string_value"):
return getattr(var_spec_msg.string_value, "message", "")
raise errors.VtsMalformedProtoStringError()
elif var_spec_msg.type == CompSpecMsg_pb2.TYPE_STRUCT:
result = {}
index = 1
for struct_value in var_spec_msg.struct_value:
if len(struct_value.name) > 0:
result[struct_value.
name] = self.GetPythonDataOfVariableSpecMsg(
struct_value)
else:
result["attribute%d" %
index] = self.GetPythonDataOfVariableSpecMsg(
struct_value)
index += 1
return result
elif var_spec_msg.type == CompSpecMsg_pb2.TYPE_UNION:
result = {}
index = 1
for union_value in var_spec_msg.union_value:
if len(union_value.name) > 0:
result[union_value.
name] = self.GetPythonDataOfVariableSpecMsg(
union_value)
else:
result["attribute%d" %
index] = self.GetPythonDataOfVariableSpecMsg(
union_value)
index += 1
return result
elif (var_spec_msg.type == CompSpecMsg_pb2.TYPE_VECTOR
or var_spec_msg.type == CompSpecMsg_pb2.TYPE_ARRAY):
result = []
for vector_value in var_spec_msg.vector_value:
result.append(
self.GetPythonDataOfVariableSpecMsg(vector_value))
return result
elif (var_spec_msg.type == CompSpecMsg_pb2.TYPE_HIDL_INTERFACE
or var_spec_msg.type == CompSpecMsg_pb2.TYPE_FMQ_SYNC
or var_spec_msg.type == CompSpecMsg_pb2.TYPE_FMQ_UNSYNC
or var_spec_msg.type == CompSpecMsg_pb2.TYPE_HIDL_MEMORY
or var_spec_msg.type == CompSpecMsg_pb2.TYPE_HANDLE):
logging.debug("var_spec_msg: %s", var_spec_msg)
return var_spec_msg
raise errors.VtsUnsupportedTypeError(
"unsupported type %s" % var_spec_msg.type)
def CallApi(self, arg, caller_uid=None):
"""RPC to CALL_API."""
self.SendCommand(SysMsg_pb2.CALL_API, arg=arg, caller_uid=caller_uid)
resp = self.RecvResponse()
resp_code = resp.response_code
if (resp_code == SysMsg_pb2.SUCCESS):
result = CompSpecMsg_pb2.FunctionSpecificationMessage()
if resp.result == "error":
raise errors.VtsTcpCommunicationError(
"API call error by the VTS driver.")
try:
text_format.Merge(resp.result, result)
except text_format.ParseError as e:
logging.exception(e)
logging.error("Paring error\n%s", resp.result)
if result.return_type.type == CompSpecMsg_pb2.TYPE_SUBMODULE:
logging.debug("returned a submodule spec")
logging.debug("spec: %s", result.return_type_submodule_spec)
return mirror_object.MirrorObject(
self, result.return_type_submodule_spec, None)
if result.HasField("return_type"): # For non-HIDL return value
result_value = result
else:
result_value = []
for return_type_hidl in result.return_type_hidl:
result_value.append(
self.GetPythonDataOfVariableSpecMsg(return_type_hidl))
if len(result.raw_coverage_data) > 0:
return result_value, {"coverage": result.raw_coverage_data}
else:
return result_value
logging.error("NOTICE - Likely a crash discovery!")
logging.error("SysMsg_pb2.SUCCESS is %s", SysMsg_pb2.SUCCESS)
raise errors.VtsTcpCommunicationError(
"RPC Error, response code for %s is %s" % (arg, resp_code))
def GetAttribute(self, arg):
"""RPC to VTS_AGENT_COMMAND_GET_ATTRIBUTE."""
self.SendCommand(SysMsg_pb2.VTS_AGENT_COMMAND_GET_ATTRIBUTE, arg=arg)
resp = self.RecvResponse()
resp_code = resp.response_code
if (resp_code == SysMsg_pb2.SUCCESS):
result = CompSpecMsg_pb2.FunctionSpecificationMessage()
if resp.result == "error":
raise errors.VtsTcpCommunicationError(
"Get attribute request failed on target.")
try:
text_format.Merge(resp.result, result)
except text_format.ParseError as e:
logging.exception(e)
logging.error("Paring error\n%s", resp.result)
if result.return_type.type == CompSpecMsg_pb2.TYPE_SUBMODULE:
logging.debug("returned a submodule spec")
logging.debug("spec: %s", result.return_type_submodule_spec)
return mirror_object.MirrorObject(
self, result.return_type_submodule_spec, None)
elif result.return_type.type == CompSpecMsg_pb2.TYPE_SCALAR:
return getattr(result.return_type.scalar_value,
result.return_type.scalar_type)
return result
logging.error("NOTICE - Likely a crash discovery!")
logging.error("SysMsg_pb2.SUCCESS is %s", SysMsg_pb2.SUCCESS)
raise errors.VtsTcpCommunicationError(
"RPC Error, response code for %s is %s" % (arg, resp_code))
def ExecuteShellCommand(self, command, no_except=False):
"""RPC to VTS_AGENT_COMMAND_EXECUTE_SHELL_COMMAND.
Args:
command: string or list or tuple, command to execute on device
no_except: bool, whether to throw exceptions. If set to True,
when exception happens, return code will be -1 and
str(err) will be in stderr. Result will maintain the
same length as with input command.
Returns:
dictionary of list, command results that contains stdout,
stderr, and exit_code.
"""
try:
return self.__ExecuteShellCommand(command)
except Exception as e:
self.error = e
if not no_except:
raise e
logging.exception(e)
return {
const.STDOUT: [""] * len(command),
const.STDERR: [str(e)] * len(command),
const.EXIT_CODE: [-1] * len(command)
}
def __ExecuteShellCommand(self, command):
"""RPC to VTS_AGENT_COMMAND_EXECUTE_SHELL_COMMAND.
Args:
command: string or list of string, command to execute on device
Returns:
dictionary of list, command results that contains stdout,
stderr, and exit_code.
"""
self.SendCommand(
SysMsg_pb2.VTS_AGENT_COMMAND_EXECUTE_SHELL_COMMAND,
shell_command=command)
resp = self.RecvResponse(retries=2)
logging.debug("resp for VTS_AGENT_COMMAND_EXECUTE_SHELL_COMMAND: %s",
resp)
stdout = []
stderr = []
exit_code = -1
if not resp:
logging.error(self.NO_RESPONSE_MSG)
stderr = [self.NO_RESPONSE_MSG]
self.error = self.NO_RESPONSE_MSG
elif resp.response_code != SysMsg_pb2.SUCCESS:
logging.error(self.FAIL_RESPONSE_MSG)
stderr = [self.FAIL_RESPONSE_MSG]
self.error = self.FAIL_RESPONSE_MSG
else:
stdout = resp.stdout_str
stderr = resp.stderr_str
exit_code = resp.exit_code
self.error = None
return {
const.STDOUT: stdout,
const.STDERR: stderr,
const.EXIT_CODE: exit_code
}
def SendFmqRequest(self, message):
"""Sends a command to the FMQ driver and receives the response.
Args:
message: FmqRequestMessage, message that contains the arguments
in the FMQ request.
Returns:
FmqResponseMessage, which includes all possible return value types,
including int, bool, and data read from the queue.
"""
self.SendCommand(SysMsg_pb2.VTS_FMQ_COMMAND, fmq_request=message)
resp = self.RecvResponse()
logging.debug("resp for VTS_FMQ_COMMAND: %s", resp)
return self.CheckResourceCommandResponse(
resp, getattr(resp, "fmq_response", None))
def SendHidlMemoryRequest(self, message):
"""Sends a command to the hidl_memory driver and receives the response.
Args:
message: HidlMemoryRequestMessage, message that contains the arguments
in the hidl_memory request.
Returns:
HidlMemoryResponseMessage, return values needed by the host-side caller.
"""
self.SendCommand(
SysMsg_pb2.VTS_HIDL_MEMORY_COMMAND, hidl_memory_request=message)
resp = self.RecvResponse()
logging.debug("resp for VTS_HIDL_MEMORY_COMMAND: %s", resp)
return self.CheckResourceCommandResponse(
resp, getattr(resp, "hidl_memory_response", None))
def SendHidlHandleRequest(self, message):
"""Sends a command to the hidl_handle driver and receives the response.
Args:
message: HidlHandleRequestMessage, message that contains the arguments
in the hidl_handle request.
Returns:
HidlHandleResponseMessage, return values needed by the host-side caller.
"""
self.SendCommand(
SysMsg_pb2.VTS_HIDL_HANDLE_COMMAND, hidl_handle_request=message)
resp = self.RecvResponse()
logging.debug("resp for VTS_HIDL_HANDLE_COMMAND: %s", resp)
return self.CheckResourceCommandResponse(
resp, getattr(resp, "hidl_handle_response", None))
@staticmethod
def CheckResourceCommandResponse(agent_response, resource_response):
"""Checks the response from a VTS resource command.
This function checks and logs framework errors such as not receiving
a response, receving a failure response. Then it checks the response
for the resource type is not None, and logs error if needed.
Args:
agent_response: AndroidSystemControlResponseMessage,
response from agent.
resource_response: FmqResponseMessage, HidlMemoryResponseMessage,
or HidlHandleResponseMessage, the response for
one of fmq, hidl_memory, and hidl_handle.
Returns:
resource_response that is passed in.
"""
if agent_response is None:
logging.error(VtsTcpClient.NO_RESPONSE_MSG)
elif agent_response.response_code != SysMsg_pb2.SUCCESS:
logging.error(VtsTcpClient.FAIL_RESPONSE_MSG)
elif resource_response is None:
logging.error("TCP client did not receive a response for " +
"the resource command.")
return resource_response
def Ping(self):
"""RPC to send a PING request.
Returns:
True if the agent is alive, False otherwise.
"""
self.SendCommand(SysMsg_pb2.PING)
resp = self.RecvResponse()
logging.debug("resp for PING: %s", resp)
if resp is not None and resp.response_code == SysMsg_pb2.SUCCESS:
return True
return False
def ReadSpecification(self,
interface_name,
target_class,
target_type,
target_version_major,
target_version_minor,
target_package,
recursive=False):
"""RPC to VTS_AGENT_COMMAND_READ_SPECIFICATION.
Args:
other args: see SendCommand
recursive: boolean, set to recursively read the imported
specification(s) and return the merged one.
"""
self.SendCommand(
SysMsg_pb2.VTS_AGENT_COMMAND_READ_SPECIFICATION,
service_name=interface_name,
target_class=target_class,
target_type=target_type,
target_version_major=target_version_major,
target_version_minor=target_version_minor,
target_package=target_package)
resp = self.RecvResponse(retries=2)
logging.debug("resp for VTS_AGENT_COMMAND_EXECUTE_READ_INTERFACE: %s",
resp)
logging.debug("proto: %s", resp.result)
result = CompSpecMsg_pb2.ComponentSpecificationMessage()
if resp.result == "error":
raise errors.VtsTcpCommunicationError(
"API call error by the VTS driver.")
try:
text_format.Merge(resp.result, result)
except text_format.ParseError as e:
logging.exception(e)
logging.error("Paring error\n%s", resp.result)
if recursive and hasattr(result, "import"):
for imported_interface in getattr(result, "import"):
if imported_interface == "android.hidl.base@1.0::types":
logging.warn("import android.hidl.base@1.0::types skipped")
continue
[package, version_str] = imported_interface.split("@")
[version_major,
version_minor] = (version_str.split("::")[0]).split(".")
imported_result = self.ReadSpecification(
imported_interface.split("::")[1],
# TODO(yim): derive target_class and
# target_type from package path or remove them
msg.component_class
if target_class is None else target_class,
msg.component_type if target_type is None else target_type,
int(version_major),
int(version_minor),
package)
# Merge the attributes from imported interface.
for attribute in imported_result.attribute:
imported_attribute = result.attribute.add()
imported_attribute.CopyFrom(attribute)
return result
def SendCommand(self,
command_type,
paths=None,
file_path=None,
bits=None,
target_class=None,
target_type=None,
target_version_major=None,
target_version_minor=None,
target_package=None,
target_component_name=None,
hw_binder_service_name=None,
is_test_hal=None,
module_name=None,
service_name=None,
callback_port=None,
driver_type=None,
shell_command=None,
caller_uid=None,
arg=None,
fmq_request=None,
hidl_memory_request=None,
hidl_handle_request=None):
"""Sends a command.
Args:
command_type: integer, the command type.
each of the other args are to fill in a field in
AndroidSystemControlCommandMessage.
"""
if not self.channel:
raise errors.VtsTcpCommunicationError(
"channel is None, unable to send command.")
command_msg = SysMsg_pb2.AndroidSystemControlCommandMessage()
command_msg.command_type = command_type
logging.debug("sending a command (type %s)",
COMMAND_TYPE_NAME[command_type])
if command_type == 202:
logging.debug("target API: %s", arg)
if target_class is not None:
command_msg.target_class = target_class
if target_type is not None:
command_msg.target_type = target_type
if target_version_major is not None:
command_msg.target_version_major = target_version_major
if target_version_minor is not None:
command_msg.target_version_minor = target_version_minor
if target_package is not None:
command_msg.target_package = target_package
if target_component_name is not None:
command_msg.target_component_name = target_component_name
if hw_binder_service_name is not None:
command_msg.hw_binder_service_name = hw_binder_service_name
if is_test_hal is not None:
command_msg.is_test_hal = is_test_hal
if module_name is not None:
command_msg.module_name = module_name
if service_name is not None:
command_msg.service_name = service_name
if driver_type is not None:
command_msg.driver_type = driver_type
if paths is not None:
command_msg.paths.extend(paths)
if file_path is not None:
command_msg.file_path = file_path
if bits is not None:
command_msg.bits = bits
if callback_port is not None:
command_msg.callback_port = callback_port
if caller_uid is not None:
command_msg.driver_caller_uid = caller_uid
if arg is not None:
command_msg.arg = arg
if shell_command is not None:
if isinstance(shell_command, (list, tuple)):
command_msg.shell_command.extend(shell_command)
else:
command_msg.shell_command.append(shell_command)
if fmq_request is not None:
command_msg.fmq_request.CopyFrom(fmq_request)
if hidl_memory_request is not None:
command_msg.hidl_memory_request.CopyFrom(hidl_memory_request)
if hidl_handle_request is not None:
command_msg.hidl_handle_request.CopyFrom(hidl_handle_request)
logging.debug("command %s" % command_msg)
message = command_msg.SerializeToString()
message_len = len(message)
logging.debug("sending %d bytes", message_len)
self.channel.write(str(message_len) + b'\n')
self.channel.write(message)
self.channel.flush()
def RecvResponse(self, retries=0):
"""Receives and parses the response, and returns the relevant ResponseMessage.
Args:
retries: an integer indicating the max number of retries in case of
session timeout error.
"""
for index in xrange(1 + retries):
try:
if index != 0:
logging.info("retrying...")
header = self.channel.readline().strip("\n")
length = int(header) if header else 0
logging.debug("resp %d bytes", length)
data = self.channel.read(length)
response_msg = SysMsg_pb2.AndroidSystemControlResponseMessage()
response_msg.ParseFromString(data)
logging.debug(
"Response %s", "success"
if response_msg.response_code == SysMsg_pb2.SUCCESS else
"fail")
return response_msg
except socket.timeout as e:
logging.exception(e)
return None