#
# Copyright (C) 2017 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 logging
from vts.proto import AndroidSystemControlMessage_pb2 as ASysCtrlMsg
from vts.runners.host import const
from vts.runners.host import errors
from vts.runners.host.tcp_client import vts_tcp_client
from vts.runners.host.tcp_server import callback_server
from vts.utils.python.mirror import hal_mirror
from vts.utils.python.mirror import lib_mirror
from vts.utils.python.mirror import shell_mirror
from vts.utils.python.mirror import resource_mirror
_DEFAULT_TARGET_BASE_PATHS = ["/system/lib64/hw"]
_DEFAULT_HWBINDER_SERVICE = "default"
_DEFAULT_SHELL_NAME = "_default"
_MAX_ADB_SHELL_LENGTH = 950
class MirrorTracker(object):
"""The class tracks all mirror objects on the host side.
Attributes:
_host_command_port: int, the host-side port for command-response
sessions.
_host_callback_port: int, the host-side port for callback sessions.
_adb: An AdbProxy object used for interacting with the device via adb.
_registered_mirrors: dict, key is mirror handler name, value is the
mirror object.
_callback_server: VtsTcpServer, the server that receives and handles
callback messages from target side.
shell_default_nohup: bool, whether to use nohup by default in shell commands.
"""
def __init__(self,
host_command_port,
host_callback_port=None,
start_callback_server=False,
adb=None):
self._host_command_port = host_command_port
self._host_callback_port = host_callback_port
self._adb = adb
self._registered_mirrors = {}
self._callback_server = None
self.shell_default_nohup = False
if start_callback_server:
self._StartCallbackServer()
def __del__(self):
self.CleanUp()
def CleanUp(self):
"""Shutdown services and release resources held by the registered mirrors.
"""
for mirror in self._registered_mirrors.values():
mirror.CleanUp()
self._registered_mirrors = {}
if self._callback_server:
self._callback_server.Stop()
self._callback_server = None
def RemoveMirror(self, mirror_name):
self._registered_mirrors[mirror_name].CleanUp()
self._registered_mirrors.pop(mirror_name)
def _StartCallbackServer(self):
"""Starts the callback server.
Raises:
errors.ComponentLoadingError is raised if the callback server fails
to start.
"""
self._callback_server = callback_server.CallbackServer()
_, port = self._callback_server.Start(self._host_callback_port)
if port != self._host_callback_port:
raise errors.ComponentLoadingError(
"Failed to start a callback TcpServer at port %s" %
self._host_callback_port)
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 = all(map(lambda shell: shell.Heal(), self._registered_mirrors.values()))
if not res:
logging.error('Self diagnosis found problems in mirror_tracker.')
return res
def InitFmq(self,
existing_queue=None,
new_queue_name=None,
data_type="uint16_t",
sync=True,
queue_size=0,
blocking=False,
reset_pointers=True,
client=None):
"""Initializes a fast message queue object.
This method will initialize a fast message queue object on the target side,
create a mirror object for the FMQ, and register it in the tracker.
Args:
existing_queue: string or MirrorObject.
This argument identifies an existing queue mirror object.
If specified, it will tell the target driver to create a
new message queue object based on an existing message queue.
If it is None, that means creating a brand new message queue.
new_queue_name: string, name of the new queue, used as key in the tracker.
If not specified, this function dynamically generates a name.
data_type: string, type of data in the queue.
sync: bool, whether the queue is synchronized (only has one reader).
queue_size: int, size of the queue.
blocking: bool, whether blocking is enabled.
reset_pointers: bool, whether to reset read/write pointers when
creating a new message queue object based on an existing message queue.
client: VtsTcpClient, if an existing session should be used.
If not specified, creates a new one.
Returns:
ResourcFmqMirror object,
it allows users to directly call methods on the mirror object.
"""
# Check if queue name already exists in tracker.
if new_queue_name is not None and new_queue_name in self._registered_mirrors:
logging.error("Queue name already exists in tracker.")
return None
# Need to initialize a client if caller doesn't provide one.
if client is None:
client = vts_tcp_client.VtsTcpClient()
client.Connect(
command_port=self._host_command_port,
callback_port=self._host_callback_port)
# Create a new queue by default.
existing_queue_id = -1
# Check if caller wants to create a queue object based on
# an existing queue object.
if existing_queue is not None:
# Check if caller provides a string.
if type(existing_queue) == str:
if existing_queue in self._registered_mirrors:
data_type = self._registered_mirrors[
existing_queue].dataType
sync = self._registered_mirrors[
existing_queue].sync
existing_queue_id = self._registered_mirrors[
existing_queue].queueId
else:
logging.error("Nonexisting queue name in mirror_tracker.")
return None
# Check if caller provides a resource mirror object.
elif isinstance(existing_queue, resource_mirror.ResourceFmqMirror):
data_type = existing_queue.dataType
sync = existing_queue.sync
existing_queue_id = existing_queue.queueId
else:
logging.error(
"Unsupported way of finding an existing queue object.")
return None
# Create a resource mirror object.
mirror = resource_mirror.ResourceFmqMirror(data_type, sync, client)
mirror._create(existing_queue_id, queue_size, blocking, reset_pointers)
if mirror.queueId == -1:
# Failed to create queue object, error logged in resource_mirror.
return None
# Needs to dynamically generate queue name if caller doesn't provide one
if new_queue_name is None:
new_queue_name = "queue_id_" + str(mirror._queue_id)
self._registered_mirrors[new_queue_name] = mirror
return mirror
def InitHidlMemory(self, mem_size=0, client=None, mem_name=None):
"""Initialize a hidl_memory object.
This method will initialize a hidl_memory object on the target side,
create a mirror object, and register it in the tracker.
Args:
mem_size: int, size of the memory region.
client: VtsTcpClient, if an existing session should be used.
If not specified, creates a new one.
mem_name: string, name of the memory region.
If not specified, dynamically assign the memory region a name.
Returns:
ResourceHidlMemoryMirror object,
it allows users to directly call methods on the mirror object.
"""
# Check if mem_name already exists in tracker.
if mem_name is not None and mem_name in self._registered_mirrors:
logging.error("Memory name already exists in tracker.")
return None
# Need to initialize a client if caller doesn't provide one.
if client is None:
client = vts_tcp_client.VtsTcpClient()
client.Connect(
command_port=self._host_command_port,
callback_port=self._host_callback_port)
# Create a resource_mirror object.
mirror = resource_mirror.ResourceHidlMemoryMirror(client)
mirror._allocate(mem_size)
if mirror.memId == -1:
# Failed to create memory object, error logged in resource_mirror.
return None
# Need to dynamically assign a memory name
# if caller doesn't provide one.
if mem_name is None:
mem_name = "mem_id_" + str(mirror._mem_id)
self._registered_mirrors[mem_name] = mirror
return mirror
def InitHidlHandleForSingleFile(self,
filepath,
mode,
ints=[],
client=None,
handle_name=None):
"""Initialize a hidl_handle object.
This method will initialize a hidl_handle object on the target side,
create a mirror object, and register it in the tracker.
TODO: Currently only support creating a handle for a single file.
In the future, need to support arbitrary file descriptor types
(e.g. socket, pipe), and more than one file.
Args:
filepath: string, path to the file.
mode: string, specifying the mode to open the file.
ints: int list, useful integers to be stored in handle object.
client: VtsTcpClient, if an existing session should be used.
If not specified, create a new one.
handle_name: string, name of the handle object.
If not specified, dynamically assign the handle object a name.
Returns:
ResourceHidlHandleMirror object,
it allows users to directly call methods on the mirror object.
"""
# Check if handle_name already exists in tracker.
if handle_name is not None and handle_name in self._registered_mirrors:
logging.error("Handle name already exists in tracker.")
return None
# Need to initialize a client if caller doesn't provide one.
if not client:
client = vts_tcp_client.VtsTcpClient()
client.Connect(
command_port=self._host_command_port,
callback_port=self._host_callback_port)
# Create a resource_mirror object.
mirror = resource_mirror.ResourceHidlHandleMirror(client)
mirror._createHandleForSingleFile(filepath, mode, ints)
if mirror.handleId == -1:
# Failed to create handle object, error logged in resource_mirror.
return None
# Need to dynamically assign a handle name
# if caller doesn't provide one.
if handle_name is None:
handle_name = "handle_id_" + str(mirror._handle_id)
self._registered_mirrors[handle_name] = mirror
return mirror
def InitHidlHal(self,
target_type,
target_version=None,
target_package=None,
target_component_name=None,
target_basepaths=_DEFAULT_TARGET_BASE_PATHS,
handler_name=None,
hw_binder_service_name=_DEFAULT_HWBINDER_SERVICE,
bits=64,
target_version_major=None,
target_version_minor=None,
is_test_hal=False):
"""Initiates a handler for a particular HIDL HAL.
This will initiate a driver service for a HAL on the target side, create
a mirror object for a HAL, and register it in the tracker.
Args:
target_type: string, the target type name (e.g., light, camera).
target_version (deprecated, now use major and minor versions):
float, the target component version (e.g., 1.0).
target_package: string, the package name of a target HIDL HAL.
target_basepaths: list of strings, the paths to look for target
files in. Default is _DEFAULT_TARGET_BASE_PATHS.
handler_name: string, the name of the handler. target_type is used
by default.
hw_binder_service_name: string, the name of a HW binder service.
bits: integer, processor architecture indicator: 32 or 64.
target_version_major:
int, the target component major version (e.g., 1.0 -> 1).
target_version_minor:
int, the target component minor version (e.g., 1.0 -> 0).
If host doesn't provide major and minor versions separately,
parse it from the float version of target_version.
is_test_hal: bool, whether the HAL service is a test HAL
(e.g. msgq).
Raises:
USERError if user doesn't provide a version of the HAL service.
"""
target_version_major, target_version_minor = self.GetTargetVersion(
target_version, target_version_major, target_version_minor)
if not handler_name:
handler_name = target_type
client = vts_tcp_client.VtsTcpClient()
client.Connect(
command_port=self._host_command_port,
callback_port=self._host_callback_port)
mirror = hal_mirror.HalMirror(client, self._callback_server)
mirror.InitHalDriver(target_type, target_version_major,
target_version_minor, target_package,
target_component_name, hw_binder_service_name,
handler_name, bits, is_test_hal)
self._registered_mirrors[target_type] = mirror
def InitSharedLib(self,
target_type,
target_version=None,
target_basepaths=_DEFAULT_TARGET_BASE_PATHS,
target_package="",
target_filename=None,
handler_name=None,
bits=64,
target_version_major=None,
target_version_minor=None):
"""Initiates a handler for a particular lib.
This will initiate a driver service for a lib on the target side, create
a mirror object for a lib, and register it in the tracker.
Args:
target_type: string, the target type name (e.g., light, camera).
target_version (deprecated, now use major and minor versions):
float, the target component version (e.g., 1.0).
target_basepaths: list of strings, the paths to look for target
files in. Default is _DEFAULT_TARGET_BASE_PATHS.
target_package: . separated string (e.g., a.b.c) to denote the
package name of target component.
target_filename: string, the target file name (e.g., libm.so).
handler_name: string, the name of the handler. target_type is used
by default.
bits: integer, processor architecture indicator: 32 or 64.
target_version_major:
int, the target component major version (e.g., 1.0 -> 1).
target_version_minor:
int, the target component minor version (e.g., 1.0 -> 0).
If host doesn't provide major and minor versions separately,
parse it from the float version of target_version.
Raises:
USERError if user doesn't provide a version of the HAL service.
"""
target_version_major, target_version_minor = self.GetTargetVersion(
target_version, target_version_major, target_version_minor)
if not handler_name:
handler_name = target_type
client = vts_tcp_client.VtsTcpClient()
client.Connect(command_port=self._host_command_port)
mirror = lib_mirror.LibMirror(client)
mirror.InitLibDriver(target_type, target_version_major,
target_version_minor, target_package,
target_filename, target_basepaths, handler_name,
bits)
self._registered_mirrors[handler_name] = mirror
def InvokeTerminal(self, instance_name, bits=32):
"""Initiates a handler for a particular shell terminal.
This will initiate a driver service for a shell on the target side,
create a mirror object for the shell, and register it in the tracker.
Args:
instance_name: string, the shell terminal instance name.
bits: integer, processor architecture indicator: 32 or 64.
"""
if not instance_name:
raise error.ComponentLoadingError("instance_name is None")
if bits not in [32, 64]:
raise error.ComponentLoadingError(
"Invalid value for bits: %s" % bits)
if instance_name in self._registered_mirrors:
logging.warning("shell driver %s already exists", instance_name)
return
client = vts_tcp_client.VtsTcpClient()
client.Connect(command_port=self._host_command_port)
logging.debug("Init the driver service for shell, %s", instance_name)
launched = client.LaunchDriverService(
driver_type=ASysCtrlMsg.VTS_DRIVER_TYPE_SHELL,
service_name="shell_" + instance_name,
bits=bits)
if not launched:
raise errors.ComponentLoadingError(
"Failed to launch shell driver service %s" % instance_name)
mirror = shell_mirror.ShellMirror(client, self._adb)
self._registered_mirrors[instance_name] = mirror
def DisableShell(self):
"""Disables all registered shell mirrors."""
for mirror in self._registered_mirrors.values():
if not isinstance(mirror, shell_mirror.ShellMirror):
logging.error("mirror object is not a shell mirror")
continue
mirror.enabled = False
def Execute(self, commands, no_except=False, nohup=None):
"""Execute shell command(s).
This method automatically decide whether to use adb shell or vts shell
driver on the device based on performance benchmark results.
The difference in the decision logic will only have impact on the performance, but
will be transparent to the user of this method.
The current logic is:
1. If shell_default_nohup is disabled and command
list size is smaller or equal than 3, use adb shell. Otherwise, use
shell driver (with nohup).
2. If adb shell is used, no_except will always be true.
This is subject to further optimization.
Args:
commands: string or list or tuple, commands 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 commands.
nohup: bool or None, True for using nohup for shell commands; False for
not using nohup; None for using default setting.
Returns:
dictionary of list, command results that contains stdout,
stderr, and exit_code.
"""
if not isinstance(commands, (list, tuple)):
commands = [commands]
if nohup is None:
nohup = self.shell_default_nohup
# TODO(yuexima): further optimize the threshold and nohup adb command
non_nohup_adb_threshold = 3
if (not nohup and len(commands) <= non_nohup_adb_threshold
and not filter(lambda cmd: len(cmd) > _MAX_ADB_SHELL_LENGTH, commands)):
return self._ExecuteShellCmdViaAdbShell(commands)
else:
return self._ExecuteShellCmdViaVtsDriver(commands, no_except)
def _ExecuteShellCmdViaVtsDriver(self, commands, no_except):
"""Execute shell command(s) using default shell terminal.
Args:
commands: string or list or tuple, commands 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.
"""
if _DEFAULT_SHELL_NAME not in self._registered_mirrors:
self.InvokeTerminal(_DEFAULT_SHELL_NAME)
return getattr(self, _DEFAULT_SHELL_NAME).Execute(commands, no_except)
def _ExecuteShellCmdViaAdbShell(self, commands):
"""Execute shell command(s) using adb shell command.
Args:
commands: string or list or tuple, command to execute on device
Returns:
dictionary of list, command results that contains stdout,
stderr, and exit_code.
"""
all = {const.STDOUT: [],
const.STDERR: [],
const.EXIT_CODE: []}
for cmd in commands:
res = self._adb.shell(cmd, no_except=True)
all[const.STDOUT].append(res[const.STDOUT])
all[const.STDERR].append(res[const.STDERR])
all[const.EXIT_CODE].append(res[const.EXIT_CODE])
return all
def SetConnTimeout(self, timeout):
"""Set remove shell connection timeout for default shell terminal.
Args:
timeout: int, TCP connection timeout in seconds.
"""
if _DEFAULT_SHELL_NAME not in self._registered_mirrors:
self.InvokeTerminal(_DEFAULT_SHELL_NAME)
getattr(self, _DEFAULT_SHELL_NAME).SetConnTimeout(timeout)
def GetTargetVersion(self, target_version, target_version_major,
target_version_minor):
"""Get the actual target version provided by the host.
If the host provides major and minor versions separately, directly return them.
Otherwise, manually parse it from the float version.
If all of them are None, raise a user error.
Args:
target_version: float, the target component HAL version (e.g. 1.0).
target_version_major:
int, the target component HAL major version (e.g. 1.0 -> 1).
target_version_minor:
int, the target component HAL minor version (e.g. 1.0 -> 0).
Returns:
two integers, actual major and minor HAL versions.
Raises: user error, if no version is provided.
"""
# Check if host provides major and minor versions separately
if (target_version_minor != None and target_version_minor != None):
return target_version_major, target_version_minor
# If not, manually parse it from float version
if (target_version != None):
target_version_str = str(target_version)
[target_version_major,
target_version_minor] = target_version_str.split(".")
return int(target_version_major), int(target_version_minor)
raise errors.USERError("User has to provide a target version.")
def GetTcpClient(self, mirror_name):
"""Gets the TCP client used in this tracker.
Useful for reusing session to access shared data.
Args:
mirror_name: used to identify mirror object.
"""
if mirror_name in self._registered_mirrors:
return self._registered_mirrors[mirror_name]._client
return None
def __getattr__(self, name):
if name in self._registered_mirrors:
return self._registered_mirrors[name]
else:
logging.error("No mirror found with name: %s", name)
return None