blob: 8079877a1d5bdc39c1b577fe0a80ba2c0f47605e [file]
# Lint as: python2, python3
# Copyright 2022 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import logging
import os
import subprocess
import grpc
from google.protobuf import empty_pb2
from autotest_lib.client.cros.tast.ui import chrome_service_pb2_grpc
from autotest_lib.client.cros.tast.ui import conn_service_pb2_grpc
from autotest_lib.client.cros.tast.ui import conn_service_pb2
# An arbitrary port number.
_TCP_PORT = 23456
class GRPC:
"""Wraps Tast gRPC APIs."""
def __init__(self, bundle_path=None):
if bundle_path is None:
bundle_path = '/usr/local/libexec/tast/bundles/local/cros'
self._bundle_path = bundle_path
def __enter__(self):
# Currently, there's no way to create gRPC service client from an FD.
# So, instead, we use the on-device TCP connection with a fixed port.
# cf) https://github.com/grpc/grpc/issues/23760
# TODO(crbug.com/1382287): Handle the case where the TCP port is
# occupied.
self._process = subprocess.Popen(
[self._bundle_path, '-rpctcp', '-port', str(_TCP_PORT)],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
self._channel = grpc.insecure_channel('localhost:%d' % _TCP_PORT)
# Wait until the channel gets available before returning.
# 30 secs should be long enough that tast gRPC is set up.
try:
grpc.channel_ready_future(self._channel).result(timeout=30)
except e:
logging.error(
'Process: ' + self._process.stdout.read().decode('utf-8'))
raise e
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self._channel.close()
self._process.kill()
self._process.wait()
@property
def channel(self):
return self._channel
def parse_proto_value(value):
"""Parses google.protobuf.struct_pb2 and returns a python object."""
if value.HasField("bool_value"):
return value.bool_value
if value.HasField("list_value"):
return [parse_proto_value(v) for v in value.list_value.values]
if value.HasField("null_value"):
return value.null_value
if value.HasField("number_value"):
return value.number_value
if value.HasField("string_value"):
return value.string_value
if value.HasField("struct_value"):
return {k: parse_proto_value(v) for (k, v) in value.struct_value.items()}
class Conn:
"""Wraps Tast gRPC ConnService APIs."""
def __init__(self, conn_service, conn_id):
self._conn_service = conn_service
self._conn_id = conn_id
def Close(self):
"""Makes a gRPC call Close to ConnService."""
self._conn_service.Close(
conn_service_pb2.CloseRequest(id=self._conn_id))
def ActivateTarget(self):
"""Makes a gRPC call ActivateTarget to ConnService."""
self._conn_service.ActivateTarget(
conn_service_pb2.ActivateTargetRequest(id=self._conn_id))
def Navigate(self, url):
"""Makes a gRPC call Navigate to ConnService."""
self._conn_service.Navigate(
conn_service_pb2.NavigateRequest(id=self._conn_id, url=url))
def Eval(self, expr):
"""Makes a gRPC call Eval to ConnService."""
response = self._conn_service.Eval(
conn_service_pb2.ConnEvalRequest(id=self._conn_id, expr=expr))
return parse_proto_value(response)
def Call(self, fn, args):
"""Makes a gRPC call Call to ConnService."""
response = self._conn_service.Call(
conn_service_pb2.ConnCallRequest(id=self._conn_id,
fn=fn,
args=args))
return parse_proto_value(response)
def WaitForExpr(self, expr, timeout_secs, fail_on_err):
"""Makes a gRPC call WaitForExpr to ConnService."""
self._conn_service.WaitForExpr(
conn_service_pb2.ConnWaitForExprRequest(
id=self._conn_id,
expr=expr,
timeout_secs=timeout_secs,
fail_on_err=fail_on_err))
class ConnService(conn_service_pb2_grpc.ConnServiceStub):
"""Wraps ConnServiceStub to call CloseAll on exit."""
def __init__(self, channel):
super().__init__(channel)
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.CloseAll(empty_pb2.Empty())
class ChromeService(chrome_service_pb2_grpc.ChromeServiceStub):
"""Wraps ChromeServiceStub to call Close on exit."""
def __init__(self, channel):
super().__init__(channel)
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
# Close makes the tast bundle disconnects from Chrome and cleans up
# standard extensions. However it does not terminate Chrome. See Close()
# on tast-tests/src/chromiumos/tast/local/chrome/chrome.go.
self.Close(empty_pb2.Empty())
# Restart ui (and thus Chrome). Do this to make the behaviour consistent
# with autotest Chrome.
self._restart_ui()
def _restart_ui(self):
logging.info('(Re)starting the ui (logs the user out)')
subprocess.run(['stop', 'ui'])
subprocess.run(['start', 'ui'])