aboutsummaryrefslogtreecommitdiff
path: root/mobly/controllers/android_device_lib/snippet_client_v2.py
diff options
context:
space:
mode:
Diffstat (limited to 'mobly/controllers/android_device_lib/snippet_client_v2.py')
-rw-r--r--mobly/controllers/android_device_lib/snippet_client_v2.py180
1 files changed, 117 insertions, 63 deletions
diff --git a/mobly/controllers/android_device_lib/snippet_client_v2.py b/mobly/controllers/android_device_lib/snippet_client_v2.py
index f7494c2..41376fb 100644
--- a/mobly/controllers/android_device_lib/snippet_client_v2.py
+++ b/mobly/controllers/android_device_lib/snippet_client_v2.py
@@ -18,7 +18,7 @@ import enum
import json
import re
import socket
-from typing import Dict
+from typing import Dict, Union
from mobly import utils
from mobly.controllers.android_device_lib import adb
@@ -28,16 +28,22 @@ from mobly.snippet import client_base
from mobly.snippet import errors
# The package of the instrumentation runner used for mobly snippet
-_INSTRUMENTATION_RUNNER_PACKAGE = 'com.google.android.mobly.snippet.SnippetRunner'
+_INSTRUMENTATION_RUNNER_PACKAGE = (
+ 'com.google.android.mobly.snippet.SnippetRunner'
+)
# The command template to start the snippet server
_LAUNCH_CMD = (
- '{shell_cmd} am instrument {user} -w -e action start {instrument_options} '
- f'{{snippet_package}}/{_INSTRUMENTATION_RUNNER_PACKAGE}')
+ '{shell_cmd} am instrument {user} -w -e action start'
+ ' {instrument_options}'
+ f' {{snippet_package}}/{_INSTRUMENTATION_RUNNER_PACKAGE}'
+)
# The command template to stop the snippet server
-_STOP_CMD = ('am instrument {user} -w -e action stop {snippet_package}/'
- f'{_INSTRUMENTATION_RUNNER_PACKAGE}')
+_STOP_CMD = (
+ 'am instrument {user} -w -e action stop {snippet_package}/'
+ f'{_INSTRUMENTATION_RUNNER_PACKAGE}'
+)
# Major version of the launch and communication protocol being used by this
# client.
@@ -89,10 +95,13 @@ class Config:
other purposes may not take effect and you should use snippet RPCs. This
is because Mobly snippet runner changes the subsequent instrumentation
process.
+ user_id: The user id under which to launch the snippet process.
"""
am_instrument_options: Dict[str, str] = dataclasses.field(
- default_factory=dict)
+ default_factory=dict
+ )
+ user_id: Union[int, None] = None
class ConnectionHandshakeCommand(enum.Enum):
@@ -106,6 +115,7 @@ class ConnectionHandshakeCommand(enum.Enum):
INIT: Initiates a new session and makes a connection with this session.
CONTINUE: Makes a connection with the current session.
"""
+
INIT = 'initiate'
CONTINUE = 'continue'
@@ -142,7 +152,7 @@ class SnippetClientV2(client_base.ClientBase):
self.device_port = None
self.uid = UNKNOWN_UID
self._adb = ad.adb
- self._user_id = None
+ self._user_id = None if config is None else config.user_id
self._proc = None
self._client = None # keep it to prevent close errors on connect failure
self._conn = None
@@ -205,19 +215,23 @@ class SnippetClientV2(client_base.ClientBase):
if not utils.grep(f'^package:{self.package}$', out):
raise errors.ServerStartPreCheckError(
self._device,
- f'{self.package} is not installed for user {self.user_id}.')
+ f'{self.package} is not installed for user {self.user_id}.',
+ )
# Validate that the app is instrumented.
out = self._adb.shell('pm list instrumentation')
matched_out = utils.grep(
f'^instrumentation:{self.package}/{_INSTRUMENTATION_RUNNER_PACKAGE}',
- out)
+ out,
+ )
if not matched_out:
raise errors.ServerStartPreCheckError(
self._device,
- f'{self.package} is installed, but it is not instrumented.')
- match = re.search(r'^instrumentation:(.*)\/(.*) \(target=(.*)\)$',
- matched_out[0])
+ f'{self.package} is installed, but it is not instrumented.',
+ )
+ match = re.search(
+ r'^instrumentation:(.*)\/(.*) \(target=(.*)\)$', matched_out[0]
+ )
target_name = match.group(3)
# Validate that the instrumentation target is installed if it's not the
# same as the snippet package.
@@ -227,14 +241,16 @@ class SnippetClientV2(client_base.ClientBase):
raise errors.ServerStartPreCheckError(
self._device,
f'Instrumentation target {target_name} is not installed for user '
- f'{self.user_id}.')
+ f'{self.user_id}.',
+ )
def _disable_hidden_api_blocklist(self):
"""If necessary and possible, disables hidden api blocklist."""
sdk_version = int(self._device.build_info['build_version_sdk'])
if self._device.is_rootable and sdk_version >= 28:
self._device.adb.shell(
- 'settings put global hidden_api_blacklist_exemptions "*"')
+ 'settings put global hidden_api_blacklist_exemptions "*"'
+ )
def start_server(self):
"""Starts the server on the remote device.
@@ -250,14 +266,19 @@ class SnippetClientV2(client_base.ClientBase):
server output.
"""
persists_shell_cmd = self._get_persisting_command()
- self.log.debug('Snippet server for package %s is using protocol %d.%d',
- self.package, _PROTOCOL_MAJOR_VERSION,
- _PROTOCOL_MINOR_VERSION)
+ self.log.debug(
+ 'Snippet server for package %s is using protocol %d.%d',
+ self.package,
+ _PROTOCOL_MAJOR_VERSION,
+ _PROTOCOL_MINOR_VERSION,
+ )
option_str = self._get_instrument_options_str()
- cmd = _LAUNCH_CMD.format(shell_cmd=persists_shell_cmd,
- user=self._get_user_command_string(),
- snippet_package=self.package,
- instrument_options=option_str)
+ cmd = _LAUNCH_CMD.format(
+ shell_cmd=persists_shell_cmd,
+ user=self._get_user_command_string(),
+ snippet_package=self.package,
+ instrument_options=option_str,
+ )
self._proc = self._run_adb_cmd(cmd)
# Check protocol version and get the device port
@@ -293,7 +314,9 @@ class SnippetClientV2(client_base.ClientBase):
'No %s and %s commands available to launch instrument '
'persistently, tests that depend on UiAutomator and '
'at the same time perform USB disconnections may fail.',
- _SETSID_COMMAND, _NOHUP_COMMAND)
+ _SETSID_COMMAND,
+ _NOHUP_COMMAND,
+ )
return ''
def _get_instrument_options_str(self):
@@ -343,15 +366,17 @@ class SnippetClientV2(client_base.ClientBase):
line = self._proc.stdout.readline().decode('utf-8')
if not line:
raise errors.ServerStartError(
- self._device, 'Unexpected EOF when waiting for server to start.')
+ self._device, 'Unexpected EOF when waiting for server to start.'
+ )
# readline() uses an empty string to mark EOF, and a single newline
# to mark regular empty lines in the output. Don't move the strip()
# call above the truthiness check, or this method will start
# considering any blank output line to be EOF.
line = line.strip()
- if (line.startswith('INSTRUMENTATION_RESULT:') or
- line.startswith('SNIPPET ')):
+ if line.startswith('INSTRUMENTATION_RESULT:') or line.startswith(
+ 'SNIPPET '
+ ):
self.log.debug('Accepted line from instrumentation output: "%s"', line)
return line
@@ -378,9 +403,17 @@ class SnippetClientV2(client_base.ClientBase):
def _forward_device_port(self):
"""Forwards the device port to a host port."""
- if not self.host_port:
- self.host_port = utils.get_available_host_port()
- self._adb.forward([f'tcp:{self.host_port}', f'tcp:{self.device_port}'])
+ if self.host_port and self.host_port in adb.list_occupied_adb_ports():
+ raise errors.Error(
+ self._device,
+ f'Cannot forward to host port {self.host_port} because adb has'
+ ' forwarded another device port to it.',
+ )
+
+ host_port = self.host_port or 0
+ # Example stdout: b'12345\n'
+ stdout = self._adb.forward([f'tcp:{host_port}', f'tcp:{self.device_port}'])
+ self.host_port = int(stdout.strip())
def create_socket_connection(self):
"""Creates a socket connection to the server.
@@ -397,23 +430,29 @@ class SnippetClientV2(client_base.ClientBase):
try:
self.log.debug(
'Snippet client is creating socket connection to the snippet server '
- 'of %s through host port %d.', self.package, self.host_port)
- self._conn = socket.create_connection(('localhost', self.host_port),
- _SOCKET_CONNECTION_TIMEOUT)
+ 'of %s through host port %d.',
+ self.package,
+ self.host_port,
+ )
+ self._conn = socket.create_connection(
+ ('localhost', self.host_port), _SOCKET_CONNECTION_TIMEOUT
+ )
except ConnectionRefusedError as err:
# Retry using '127.0.0.1' for IPv4 enabled machines that only resolve
# 'localhost' to '[::1]'.
- self.log.debug('Failed to connect to localhost, trying 127.0.0.1: %s',
- str(err))
- self._conn = socket.create_connection(('127.0.0.1', self.host_port),
- _SOCKET_CONNECTION_TIMEOUT)
+ self.log.debug(
+ 'Failed to connect to localhost, trying 127.0.0.1: %s', str(err)
+ )
+ self._conn = socket.create_connection(
+ ('127.0.0.1', self.host_port), _SOCKET_CONNECTION_TIMEOUT
+ )
self._conn.settimeout(_SOCKET_READ_TIMEOUT)
self._client = self._conn.makefile(mode='brw')
- def send_handshake_request(self,
- uid=UNKNOWN_UID,
- cmd=ConnectionHandshakeCommand.INIT):
+ def send_handshake_request(
+ self, uid=UNKNOWN_UID, cmd=ConnectionHandshakeCommand.INIT
+ ):
"""Sends a handshake request to the server to prepare for the communication.
Through the handshake response, this function checks whether the server
@@ -438,7 +477,8 @@ class SnippetClientV2(client_base.ClientBase):
if not response:
raise errors.ProtocolError(
- self._device, errors.ProtocolError.NO_RESPONSE_FROM_HANDSHAKE)
+ self._device, errors.ProtocolError.NO_RESPONSE_FROM_HANDSHAKE
+ )
response = self._decode_socket_response_bytes(response)
@@ -471,8 +511,9 @@ class SnippetClientV2(client_base.ClientBase):
self._client_send(request)
response = self._client_receive()
if not response:
- raise errors.ProtocolError(self._device,
- errors.ProtocolError.NO_RESPONSE_FROM_SERVER)
+ raise errors.ProtocolError(
+ self._device, errors.ProtocolError.NO_RESPONSE_FROM_SERVER
+ )
return self._decode_socket_response_bytes(response)
def _client_send(self, message):
@@ -490,7 +531,7 @@ class SnippetClientV2(client_base.ClientBase):
except socket.error as e:
raise errors.Error(
self._device,
- f'Encountered socket error "{e}" sending RPC message "{message}"'
+ f'Encountered socket error "{e}" sending RPC message "{message}"',
) from e
def _client_receive(self):
@@ -506,8 +547,8 @@ class SnippetClientV2(client_base.ClientBase):
return self._client.readline()
except socket.error as e:
raise errors.Error(
- self._device,
- f'Encountered socket error "{e}" reading RPC response') from e
+ self._device, f'Encountered socket error "{e}" reading RPC response'
+ ) from e
def _decode_socket_response_bytes(self, response):
"""Returns a string decoded from the socket response bytes.
@@ -525,8 +566,9 @@ class SnippetClientV2(client_base.ClientBase):
return str(response, encoding='utf8')
except UnicodeError:
self.log.error(
- 'Failed to decode socket response bytes using encoding '
- 'utf8: %s', response)
+ 'Failed to decode socket response bytes using encoding utf8: %s',
+ response,
+ )
raise
def handle_callback(self, callback_id, ret_value, rpc_func_name):
@@ -552,7 +594,8 @@ class SnippetClientV2(client_base.ClientBase):
method_name=rpc_func_name,
device=self._device,
rpc_max_timeout_sec=_SOCKET_READ_TIMEOUT,
- default_timeout_sec=_CALLBACK_DEFAULT_TIMEOUT_SEC)
+ default_timeout_sec=_CALLBACK_DEFAULT_TIMEOUT_SEC,
+ )
def _create_event_client(self):
"""Creates a separate client to the same session for propagating events.
@@ -563,14 +606,19 @@ class SnippetClientV2(client_base.ClientBase):
"""
self._event_client = SnippetClientV2(package=self.package, ad=self._device)
self._event_client.make_connection_with_forwarded_port(
- self.host_port, self.device_port, self.uid,
- ConnectionHandshakeCommand.CONTINUE)
-
- def make_connection_with_forwarded_port(self,
- host_port,
- device_port,
- uid=UNKNOWN_UID,
- cmd=ConnectionHandshakeCommand.INIT):
+ self.host_port,
+ self.device_port,
+ self.uid,
+ ConnectionHandshakeCommand.CONTINUE,
+ )
+
+ def make_connection_with_forwarded_port(
+ self,
+ host_port,
+ device_port,
+ uid=UNKNOWN_UID,
+ cmd=ConnectionHandshakeCommand.INIT,
+ ):
"""Makes a connection to the server with the given forwarded port.
This process assumes that a device port has already been forwarded to a
@@ -653,13 +701,16 @@ class SnippetClientV2(client_base.ClientBase):
# Send the stop signal to the server running on the device side.
out = self._adb.shell(
- _STOP_CMD.format(snippet_package=self.package,
- user=self._get_user_command_string())).decode('utf-8')
+ _STOP_CMD.format(
+ snippet_package=self.package, user=self._get_user_command_string()
+ )
+ ).decode('utf-8')
if 'OK (0 tests)' not in out:
raise android_device_lib_errors.DeviceError(
self._device,
- f'Failed to stop existing apk. Unexpected output: {out}.')
+ f'Failed to stop existing apk. Unexpected output: {out}.',
+ )
def _destroy_event_client(self):
"""Releases all the resources acquired in `_create_event_client`."""
@@ -699,9 +750,11 @@ class SnippetClientV2(client_base.ClientBase):
self.log.error('Failed to re-connect to the server.')
raise errors.ServerRestoreConnectionError(
self._device,
- (f'Failed to restore server connection for {self.package} at '
- f'host port {self.host_port}, device port {self.device_port}.'
- )) from e
+ (
+ f'Failed to restore server connection for {self.package} at '
+ f'host port {self.host_port}, device port {self.device_port}.'
+ ),
+ ) from e
# Because the previous connection was lost, update self._proc
self._proc = None
@@ -719,7 +772,8 @@ class SnippetClientV2(client_base.ClientBase):
"""
if self._event_client:
self._event_client.make_connection_with_forwarded_port(
- self.host_port, self.device_port)
+ self.host_port, self.device_port
+ )
def help(self, print_output=True):
"""Calls the help RPC, which returns the list of RPC calls available.