diff --git a/src/devtools/mobileharness/infra/container/proto/test_engine.proto b/src/devtools/mobileharness/infra/container/proto/test_engine.proto index 8add37c681..3ec71a5cb1 100644 --- a/src/devtools/mobileharness/infra/container/proto/test_engine.proto +++ b/src/devtools/mobileharness/infra/container/proto/test_engine.proto @@ -31,6 +31,9 @@ message TestEngineLocator { GrpcLocator grpc_locator = 5; bool enable_grpc = 6; + DualConduitLocator dual_conduit_locator = 7; + bool enable_dual_conduit = 8; + message StubbyLocator { // Optional. A test engine may report no or more than one IPs. If so, client // should use the IP of the lab server (which is detected by MH master) @@ -63,6 +66,13 @@ message TestEngineLocator { // Since lab 4.270.0. string host_ip = 4; } + + message DualConduitLocator { + // Required. + // + // An xDS address like xds:///myserver.hostname.dcon. + string xds_address = 1; + } } enum TestEngineStatus { diff --git a/src/java/com/google/devtools/mobileharness/infra/lab/BUILD b/src/java/com/google/devtools/mobileharness/infra/lab/BUILD index 242889a613..b67d4e0dbf 100644 --- a/src/java/com/google/devtools/mobileharness/infra/lab/BUILD +++ b/src/java/com/google/devtools/mobileharness/infra/lab/BUILD @@ -123,6 +123,9 @@ java_library( "//src/java/com/google/devtools/mobileharness/shared/util/auto:auto_value", "//src/java/com/google/devtools/mobileharness/shared/util/base", "//src/java/com/google/devtools/mobileharness/shared/util/base:proto_text_format", + "//src/java/com/google/devtools/mobileharness/shared/util/comm/dualconduit/client", + "//src/java/com/google/devtools/mobileharness/shared/util/comm/dualconduit/proxy", + "//src/java/com/google/devtools/mobileharness/shared/util/comm/dualconduit/proxy:server_type", "//src/java/com/google/devtools/mobileharness/shared/util/comm/filetransfer/cloud/rpc/service:cloud_file_transfer_service_grpc_impl", "//src/java/com/google/devtools/mobileharness/shared/util/comm/filetransfer/cloud/rpc/service:cloud_file_transfer_service_impl", "//src/java/com/google/devtools/mobileharness/shared/util/comm/filetransfer/common:tagged_file_handler", diff --git a/src/java/com/google/devtools/mobileharness/infra/lab/LabServer.java b/src/java/com/google/devtools/mobileharness/infra/lab/LabServer.java index 1f974b5700..ee7ec85736 100644 --- a/src/java/com/google/devtools/mobileharness/infra/lab/LabServer.java +++ b/src/java/com/google/devtools/mobileharness/infra/lab/LabServer.java @@ -84,6 +84,9 @@ import com.google.devtools.mobileharness.shared.labinfo.LocalLabInfoProvider; import com.google.devtools.mobileharness.shared.util.base.ProtoTextFormat; import com.google.devtools.mobileharness.shared.util.base.StrUtil; +import com.google.devtools.mobileharness.shared.util.comm.dualconduit.client.DualConduitClient; +import com.google.devtools.mobileharness.shared.util.comm.dualconduit.proxy.ReverseProxiedServer; +import com.google.devtools.mobileharness.shared.util.comm.dualconduit.proxy.ServerType; import com.google.devtools.mobileharness.shared.util.comm.filetransfer.cloud.rpc.service.CloudFileTransferServiceGrpcImpl; import com.google.devtools.mobileharness.shared.util.comm.filetransfer.cloud.rpc.service.CloudFileTransferServiceImpl; import com.google.devtools.mobileharness.shared.util.comm.filetransfer.common.TaggedFileHandler; @@ -115,6 +118,7 @@ import com.google.wireless.qa.mobileharness.shared.util.DeviceUtil; import com.google.wireless.qa.mobileharness.shared.util.NetUtil; import io.grpc.BindableService; +import io.grpc.Server; import io.grpc.netty.NettyServerBuilder; import io.grpc.protobuf.services.ProtoReflectionService; import java.io.IOException; @@ -153,6 +157,8 @@ public class LabServer { private final boolean enableStubbyRpcServer; private final int rpcPort; + private volatile Server localGrpcServer; + @Inject LabServer( ProxyTestManager testManager, @@ -373,7 +379,25 @@ ListenableFuture provideLocalDeviceManager() { for (BindableService service : localGrpcServices) { localGrpcServerBuilder.addService(service); } - localGrpcServerBuilder.build().start(); + Server server = localGrpcServerBuilder.build(); + if (Flags.dconDialerAddress.get() != null + && !Flags.dconDialerAddress.getNonNull().isEmpty()) { + String instanceId = netUtil.getParentHostname(); + String localHostname = Flags.localHostname.getNonNull(); + + DualConduitClient client = new DualConduitClient(Flags.dconDialerAddress.getNonNull()); + + localGrpcServer = + new ReverseProxiedServer( + server, + client, + ServerType.LAB_SERVER.name().toLowerCase(), + instanceId, + localHostname); + } else { + localGrpcServer = server; + } + localGrpcServer.start(); // NOTE: Only for debug/test purpose. if (Flags.debugRandomExit.getNonNull()) { @@ -421,6 +445,10 @@ public ListenableFuture getStartingFuture() { public void onShutdown() { logger.atInfo().log("Lab server is shutting down."); + if (localGrpcServer != null) { + localGrpcServer.shutdown(); + } + SystemUtil.setProcessIsShuttingDown(); // Shuts down the server and all threads here. if (mainThreadPool != null) { diff --git a/src/java/com/google/devtools/mobileharness/shared/util/comm/dualconduit/client/BUILD b/src/java/com/google/devtools/mobileharness/shared/util/comm/dualconduit/client/BUILD new file mode 100644 index 0000000000..8ae883c494 --- /dev/null +++ b/src/java/com/google/devtools/mobileharness/shared/util/comm/dualconduit/client/BUILD @@ -0,0 +1,35 @@ +# Copyright 2022 Google LLC +# +# 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 +# +# https://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. +# + +load("@rules_java//java:defs.bzl", "java_library") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = [ + "//:deviceinfra_all_pkg", + ], +) + +java_library( + name = "client", + srcs = ["DualConduitClient.java"], + deps = [ + "//src/devtools/mobileharness/shared/util/comm/dualconduit/proto:dual_conduit_java_proto", + "//src/devtools/mobileharness/shared/util/comm/dualconduit/proto:dual_conduit_service_java_grpc", + "@grpc-java//core", + "@maven//:com_google_code_findbugs_jsr305", + "@maven//:com_google_guava_guava", + ], +) diff --git a/src/java/com/google/devtools/mobileharness/shared/util/comm/dualconduit/client/DualConduitClient.java b/src/java/com/google/devtools/mobileharness/shared/util/comm/dualconduit/client/DualConduitClient.java new file mode 100644 index 0000000000..a02eb40167 --- /dev/null +++ b/src/java/com/google/devtools/mobileharness/shared/util/comm/dualconduit/client/DualConduitClient.java @@ -0,0 +1,101 @@ +/* + * Copyright 2022 Google LLC + * + * 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 + * + * https://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. + */ + +package com.google.devtools.mobileharness.shared.util.comm.dualconduit.client; + +import com.google.common.util.concurrent.ListenableFuture; +import com.google.devtools.mobileharness.shared.util.comm.dualconduit.proto.DualConduitProto.EstablishConduitRequest; +import com.google.devtools.mobileharness.shared.util.comm.dualconduit.proto.DualConduitProto.EstablishConduitRequest.ConduitType; +import com.google.devtools.mobileharness.shared.util.comm.dualconduit.proto.DualConduitProto.EstablishConduitRequest.Protocol; +import com.google.devtools.mobileharness.shared.util.comm.dualconduit.proto.DualConduitProto.EstablishConduitResponse; +import com.google.devtools.mobileharness.shared.util.comm.dualconduit.proto.DualConduitProto.TeardownConduitRequest; +import com.google.devtools.mobileharness.shared.util.comm.dualconduit.proto.DualConduitProto.TeardownConduitResponse; +import com.google.devtools.mobileharness.shared.util.comm.dualconduit.proto.DualConduitServiceGrpc; +import io.grpc.Channel; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import javax.annotation.Nullable; + +/** Client for DualConduitService. */ +public class DualConduitClient { + + private final DualConduitServiceGrpc.DualConduitServiceBlockingStub blockingStub; + private final DualConduitServiceGrpc.DualConduitServiceFutureStub futureStub; + @Nullable private final ManagedChannel managedChannel; + + public DualConduitClient(Channel channel) { + this.blockingStub = DualConduitServiceGrpc.newBlockingStub(channel); + this.futureStub = DualConduitServiceGrpc.newFutureStub(channel); + this.managedChannel = null; + } + + public DualConduitClient(String target) { + this.managedChannel = ManagedChannelBuilder.forTarget(target).usePlaintext().build(); + this.blockingStub = DualConduitServiceGrpc.newBlockingStub(managedChannel); + this.futureStub = DualConduitServiceGrpc.newFutureStub(managedChannel); + } + + /** Shuts down the managed channel if it was created by this client. */ + public void shutdown() { + if (managedChannel != null) { + managedChannel.shutdown(); + } + } + + /** Establishes a gRPC reverse conduit. */ + public EstablishConduitResponse establishReverseGrpcConduit( + String serverName, String instanceId, String destinationEndpoint) { + EstablishConduitRequest request = + EstablishConduitRequest.newBuilder() + .setType(ConduitType.CONDUIT_TYPE_REVERSE) + .setProtocol(Protocol.PROTOCOL_GRPC) + .setAutoReconnect(true) + .setServerName(serverName) + .setInstanceId(instanceId) + .setDestinationEndpoint(destinationEndpoint) + .build(); + return blockingStub.establishConduit(request); + } + + /** Establishes a gRPC reverse conduit asynchronously. */ + public ListenableFuture establishReverseGrpcConduitAsync( + String serverName, String instanceId, String destinationEndpoint) { + EstablishConduitRequest request = + EstablishConduitRequest.newBuilder() + .setType(ConduitType.CONDUIT_TYPE_REVERSE) + .setProtocol(Protocol.PROTOCOL_GRPC) + .setAutoReconnect(true) + .setServerName(serverName) + .setInstanceId(instanceId) + .setDestinationEndpoint(destinationEndpoint) + .build(); + return futureStub.establishConduit(request); + } + + /** Tears down a conduit. */ + public TeardownConduitResponse teardownConduit(String conduitId) { + TeardownConduitRequest request = + TeardownConduitRequest.newBuilder().setConduitId(conduitId).build(); + return blockingStub.teardownConduit(request); + } + + /** Tears down a conduit asynchronously. */ + public ListenableFuture teardownConduitAsync(String conduitId) { + TeardownConduitRequest request = + TeardownConduitRequest.newBuilder().setConduitId(conduitId).build(); + return futureStub.teardownConduit(request); + } +} diff --git a/src/java/com/google/devtools/mobileharness/shared/util/comm/dualconduit/proxy/BUILD b/src/java/com/google/devtools/mobileharness/shared/util/comm/dualconduit/proxy/BUILD new file mode 100644 index 0000000000..9b57c71e63 --- /dev/null +++ b/src/java/com/google/devtools/mobileharness/shared/util/comm/dualconduit/proxy/BUILD @@ -0,0 +1,40 @@ +# Copyright 2022 Google LLC +# +# 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 +# +# https://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. +# + +load("@rules_java//java:defs.bzl", "java_library") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = [ + "//:deviceinfra_all_pkg", + ], +) + +java_library( + name = "proxy", + srcs = ["ReverseProxiedServer.java"], + deps = [ + "//src/devtools/mobileharness/shared/util/comm/dualconduit/proto:dual_conduit_java_proto", + "//src/java/com/google/devtools/mobileharness/shared/util/comm/dualconduit/client", + "//src/java/com/google/devtools/mobileharness/shared/util/logging:google_logger", + "@grpc-java//core", + ], +) + +java_library( + name = "server_type", + srcs = ["ServerType.java"], + deps = [], +) diff --git a/src/java/com/google/devtools/mobileharness/shared/util/comm/dualconduit/proxy/ReverseProxiedServer.java b/src/java/com/google/devtools/mobileharness/shared/util/comm/dualconduit/proxy/ReverseProxiedServer.java new file mode 100644 index 0000000000..f33e727d72 --- /dev/null +++ b/src/java/com/google/devtools/mobileharness/shared/util/comm/dualconduit/proxy/ReverseProxiedServer.java @@ -0,0 +1,121 @@ +/* + * Copyright 2022 Google LLC + * + * 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 + * + * https://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. + */ + +package com.google.devtools.mobileharness.shared.util.comm.dualconduit.proxy; + +import com.google.common.flogger.FluentLogger; +import com.google.devtools.mobileharness.shared.util.comm.dualconduit.client.DualConduitClient; +import com.google.devtools.mobileharness.shared.util.comm.dualconduit.proto.DualConduitProto.EstablishConduitResponse; +import io.grpc.Server; +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +/** + * A decorator for {@link io.grpc.Server} that establishes a DualConduit after starting and tears it + * down before shutting down. + */ +public class ReverseProxiedServer extends Server { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + private final Server delegate; + private final DualConduitClient client; + private final String serverName; + private final String instanceId; + private final String localHostname; + private String conduitId; + + public ReverseProxiedServer( + Server delegate, + DualConduitClient client, + String serverName, + String instanceId, + String localHostname) { + this.delegate = delegate; + this.client = client; + this.serverName = serverName; + this.instanceId = instanceId; + this.localHostname = localHostname; + } + + @Override + public Server start() throws IOException { + delegate.start(); + logger.atInfo().log("Delegate server started, establishing conduit..."); + try { + String destinationEndpoint = localHostname + ":" + delegate.getPort(); + EstablishConduitResponse response = + client.establishReverseGrpcConduit(serverName, instanceId, destinationEndpoint); + this.conduitId = response.getConduitId(); + logger.atInfo().log("Conduit established, conduitId: %s", conduitId); + } catch (RuntimeException e) { + delegate.shutdown(); + throw new IOException("Failed to establish conduit", e); + } + return this; + } + + @Override + public Server shutdown() { + logger.atInfo().log("Shutting down ReverseProxiedServer..."); + if (conduitId != null) { + try { + logger.atInfo().log("Tearing down conduit %s", conduitId); + var unused = client.teardownConduit(conduitId); + } catch (RuntimeException e) { + logger.atWarning().withCause(e).log("Failed to teardown conduit %s", conduitId); + } + } + client.shutdown(); + delegate.shutdown(); + return this; + } + + @Override + public Server shutdownNow() { + logger.atInfo().log("Shutting down ReverseProxiedServer now..."); + if (conduitId != null) { + try { + logger.atInfo().log("Tearing down conduit %s", conduitId); + var unused = client.teardownConduit(conduitId); + } catch (RuntimeException e) { + logger.atWarning().withCause(e).log("Failed to teardown conduit %s", conduitId); + } + } + client.shutdown(); + delegate.shutdownNow(); + return this; + } + + @Override + public boolean isShutdown() { + return delegate.isShutdown(); + } + + @Override + public boolean isTerminated() { + return delegate.isTerminated(); + } + + @Override + public void awaitTermination() throws InterruptedException { + delegate.awaitTermination(); + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + return delegate.awaitTermination(timeout, unit); + } +} diff --git a/src/java/com/google/devtools/mobileharness/shared/util/comm/dualconduit/proxy/ServerType.java b/src/java/com/google/devtools/mobileharness/shared/util/comm/dualconduit/proxy/ServerType.java new file mode 100644 index 0000000000..e141ef1b2f --- /dev/null +++ b/src/java/com/google/devtools/mobileharness/shared/util/comm/dualconduit/proxy/ServerType.java @@ -0,0 +1,25 @@ +/* + * Copyright 2022 Google LLC + * + * 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 + * + * https://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. + */ + +package com.google.devtools.mobileharness.shared.util.comm.dualconduit.proxy; + +/** Enum of server type */ +public enum ServerType { + /** Unspecified server type. */ + SERVER_TYPE_UNSPECIFIED, + LAB_SERVER, + TEST_ENGINE; +} diff --git a/src/java/com/google/devtools/mobileharness/shared/util/flags/Flags.java b/src/java/com/google/devtools/mobileharness/shared/util/flags/Flags.java index b47657bd7a..ed50476b45 100644 --- a/src/java/com/google/devtools/mobileharness/shared/util/flags/Flags.java +++ b/src/java/com/google/devtools/mobileharness/shared/util/flags/Flags.java @@ -1202,6 +1202,12 @@ public enum MasterDatabaseBackend { @FlagSpec(name = "mhproxy_spec", help = "GSLB blade target for MH Proxy.") public static final Flag mhProxySpec = Flag.value(""); + @FlagSpec(name = "dcon_dialer_address", help = "The address of the DualConduit dialer server.") + public static final Flag dconDialerAddress = Flag.value(""); + + @FlagSpec(name = "local_hostname", help = "The local hostname of the registered server.") + public static final Flag localHostname = Flag.value("localhost"); + @FlagSpec( name = "monitor_cloudrpc", help = "Whether enable the cloudrpc monitor. default is true.") diff --git a/src/java/com/google/devtools/mobileharness/shared/util/network/NetworkUtil.java b/src/java/com/google/devtools/mobileharness/shared/util/network/NetworkUtil.java index c9a952ca84..6c21d0d892 100644 --- a/src/java/com/google/devtools/mobileharness/shared/util/network/NetworkUtil.java +++ b/src/java/com/google/devtools/mobileharness/shared/util/network/NetworkUtil.java @@ -44,6 +44,19 @@ public String getLocalHostName() throws MobileHarnessException { } } + /** + * Returns the parent host name if running in a container, or the local host name otherwise. + * + * @throws MobileHarnessException if the local host name could not be resolved into an address + */ + public String getParentHostname() throws MobileHarnessException { + String parentHostname = System.getenv("PARENT_HOSTNAME"); + if (parentHostname != null && !parentHostname.isEmpty()) { + return parentHostname; + } + return getLocalHostName(); + } + /** * Returns the IP addresses from all network interfaces, except: * diff --git a/src/java/com/google/wireless/qa/mobileharness/shared/util/NetUtil.java b/src/java/com/google/wireless/qa/mobileharness/shared/util/NetUtil.java index 4b7a9eb3ac..ffe8580a26 100644 --- a/src/java/com/google/wireless/qa/mobileharness/shared/util/NetUtil.java +++ b/src/java/com/google/wireless/qa/mobileharness/shared/util/NetUtil.java @@ -103,6 +103,19 @@ public String getLocalHostName() throws MobileHarnessException { } } + /** + * Returns the parent host name if running in a container, or the local host name otherwise. + * + * @throws MobileHarnessException if the local host name could not be resolved into an address + */ + public String getParentHostname() throws MobileHarnessException { + try { + return newUtil.getParentHostname(); + } catch (MobileHarnessException e) { + throw new MobileHarnessException(BasicErrorId.LOCAL_NETWORK_ERROR, e.getMessage(), e); + } + } + public Optional getLocalHostLocation() throws MobileHarnessException { return Optional.empty(); } diff --git a/src/javatests/com/google/devtools/mobileharness/shared/util/comm/dualconduit/client/BUILD b/src/javatests/com/google/devtools/mobileharness/shared/util/comm/dualconduit/client/BUILD new file mode 100644 index 0000000000..ed7933927c --- /dev/null +++ b/src/javatests/com/google/devtools/mobileharness/shared/util/comm/dualconduit/client/BUILD @@ -0,0 +1,40 @@ +# Copyright 2022 Google LLC +# +# 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 +# +# https://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. +# + +load("@rules_java//java:defs.bzl", "java_test") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = [ + "//:deviceinfra_all_pkg", + ], +) + +java_test( + name = "DualConduitClientTest", + srcs = ["DualConduitClientTest.java"], + deps = [ + "//src/devtools/mobileharness/shared/util/comm/dualconduit/proto:dual_conduit_java_proto", + "//src/devtools/mobileharness/shared/util/comm/dualconduit/proto:dual_conduit_service_java_grpc", + "//src/java/com/google/devtools/mobileharness/shared/util/comm/dualconduit/client", + "//src/javatests/com/google/devtools/mobileharness/builddefs:truth", + "@grpc-java//core", + "@grpc-java//core:inprocess", + "@grpc-java//stub", + "@grpc-java//testing", + "@maven//:com_google_guava_guava", + "@maven//:junit_junit", + ], +) diff --git a/src/javatests/com/google/devtools/mobileharness/shared/util/comm/dualconduit/client/DualConduitClientTest.java b/src/javatests/com/google/devtools/mobileharness/shared/util/comm/dualconduit/client/DualConduitClientTest.java new file mode 100644 index 0000000000..c417edef1e --- /dev/null +++ b/src/javatests/com/google/devtools/mobileharness/shared/util/comm/dualconduit/client/DualConduitClientTest.java @@ -0,0 +1,146 @@ +/* + * Copyright 2022 Google LLC + * + * 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 + * + * https://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. + */ + +package com.google.devtools.mobileharness.shared.util.comm.dualconduit.client; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.util.concurrent.ListenableFuture; +import com.google.devtools.mobileharness.shared.util.comm.dualconduit.proto.DualConduitProto.EstablishConduitRequest; +import com.google.devtools.mobileharness.shared.util.comm.dualconduit.proto.DualConduitProto.EstablishConduitRequest.ConduitType; +import com.google.devtools.mobileharness.shared.util.comm.dualconduit.proto.DualConduitProto.EstablishConduitRequest.Protocol; +import com.google.devtools.mobileharness.shared.util.comm.dualconduit.proto.DualConduitProto.EstablishConduitResponse; +import com.google.devtools.mobileharness.shared.util.comm.dualconduit.proto.DualConduitProto.ServiceLocator; +import com.google.devtools.mobileharness.shared.util.comm.dualconduit.proto.DualConduitProto.TeardownConduitRequest; +import com.google.devtools.mobileharness.shared.util.comm.dualconduit.proto.DualConduitProto.TeardownConduitResponse; +import com.google.devtools.mobileharness.shared.util.comm.dualconduit.proto.DualConduitServiceGrpc; +import io.grpc.Channel; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.stub.StreamObserver; +import io.grpc.testing.GrpcCleanupRule; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class DualConduitClientTest { + + @Rule public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); + + private DualConduitClient client; + private final FakeDualConduitService service = new FakeDualConduitService(); + + @Before + public void setUp() throws Exception { + String serverName = InProcessServerBuilder.generateName(); + grpcCleanup.register( + InProcessServerBuilder.forName(serverName) + .directExecutor() + .addService(service) + .build() + .start()); + Channel channel = + grpcCleanup.register(InProcessChannelBuilder.forName(serverName).directExecutor().build()); + client = new DualConduitClient(channel); + } + + @Test + public void establishReverseGrpcConduit_success() { + EstablishConduitResponse response = + client.establishReverseGrpcConduit("my-server", "localhost", "backend:50051"); + + assertThat(response.getConduitId()).isEqualTo("fake-conduit-id"); + assertThat(response.getServiceLocator().getXdsAddress()).isEqualTo("xds:///my-server.dcon"); + + EstablishConduitRequest recordedRequest = service.getRecordedRequest(); + assertThat(recordedRequest.getType()).isEqualTo(ConduitType.CONDUIT_TYPE_REVERSE); + assertThat(recordedRequest.getProtocol()).isEqualTo(Protocol.PROTOCOL_GRPC); + assertThat(recordedRequest.getServerName()).isEqualTo("my-server"); + assertThat(recordedRequest.getInstanceId()).isEqualTo("localhost"); + assertThat(recordedRequest.getDestinationEndpoint()).isEqualTo("backend:50051"); + } + + @Test + public void establishReverseGrpcConduitAsync_success() throws Exception { + ListenableFuture future = + client.establishReverseGrpcConduitAsync("my-server", "localhost", "backend:50051"); + + EstablishConduitResponse response = future.get(); + + assertThat(response.getConduitId()).isEqualTo("fake-conduit-id"); + assertThat(response.getServiceLocator().getXdsAddress()).isEqualTo("xds:///my-server.dcon"); + } + + @Test + public void teardownConduit_success() { + TeardownConduitResponse response = client.teardownConduit("fake-conduit-id"); + + assertThat(response).isNotNull(); + TeardownConduitRequest recordedRequest = service.getRecordedTeardownRequest(); + assertThat(recordedRequest.getConduitId()).isEqualTo("fake-conduit-id"); + } + + @Test + public void teardownConduitAsync_success() throws Exception { + ListenableFuture future = + client.teardownConduitAsync("fake-conduit-id"); + + TeardownConduitResponse response = future.get(); + + assertThat(response).isNotNull(); + } + + private static class FakeDualConduitService + extends DualConduitServiceGrpc.DualConduitServiceImplBase { + private EstablishConduitRequest recordedRequest; + private TeardownConduitRequest recordedTeardownRequest; + + @Override + public void establishConduit( + EstablishConduitRequest request, + StreamObserver responseObserver) { + this.recordedRequest = request; + EstablishConduitResponse response = + EstablishConduitResponse.newBuilder() + .setConduitId("fake-conduit-id") + .setServiceLocator( + ServiceLocator.newBuilder().setXdsAddress("xds:///my-server.dcon").build()) + .build(); + responseObserver.onNext(response); + responseObserver.onCompleted(); + } + + @Override + public void teardownConduit( + TeardownConduitRequest request, StreamObserver responseObserver) { + this.recordedTeardownRequest = request; + TeardownConduitResponse response = TeardownConduitResponse.getDefaultInstance(); + responseObserver.onNext(response); + responseObserver.onCompleted(); + } + + EstablishConduitRequest getRecordedRequest() { + return recordedRequest; + } + + TeardownConduitRequest getRecordedTeardownRequest() { + return recordedTeardownRequest; + } + } +} diff --git a/src/javatests/com/google/devtools/mobileharness/shared/util/comm/dualconduit/proxy/BUILD b/src/javatests/com/google/devtools/mobileharness/shared/util/comm/dualconduit/proxy/BUILD new file mode 100644 index 0000000000..3971960ead --- /dev/null +++ b/src/javatests/com/google/devtools/mobileharness/shared/util/comm/dualconduit/proxy/BUILD @@ -0,0 +1,37 @@ +# Copyright 2022 Google LLC +# +# 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 +# +# https://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. +# + +load("@rules_java//java:defs.bzl", "java_test") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = [ + "//:deviceinfra_all_pkg", + ], +) + +java_test( + name = "ReverseProxiedServerTest", + srcs = ["ReverseProxiedServerTest.java"], + deps = [ + "//src/devtools/mobileharness/shared/util/comm/dualconduit/proto:dual_conduit_java_proto", + "//src/java/com/google/devtools/mobileharness/shared/util/comm/dualconduit/client", + "//src/java/com/google/devtools/mobileharness/shared/util/comm/dualconduit/proxy", + "//src/javatests/com/google/devtools/mobileharness/builddefs:truth", + "@grpc-java//core", + "@maven//:junit_junit", + "@maven//:org_mockito_mockito_core", + ], +) diff --git a/src/javatests/com/google/devtools/mobileharness/shared/util/comm/dualconduit/proxy/ReverseProxiedServerTest.java b/src/javatests/com/google/devtools/mobileharness/shared/util/comm/dualconduit/proxy/ReverseProxiedServerTest.java new file mode 100644 index 0000000000..1cccc955da --- /dev/null +++ b/src/javatests/com/google/devtools/mobileharness/shared/util/comm/dualconduit/proxy/ReverseProxiedServerTest.java @@ -0,0 +1,119 @@ +/* + * Copyright 2022 Google LLC + * + * 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 + * + * https://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. + */ + +package com.google.devtools.mobileharness.shared.util.comm.dualconduit.proxy; + +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.devtools.mobileharness.shared.util.comm.dualconduit.client.DualConduitClient; +import com.google.devtools.mobileharness.shared.util.comm.dualconduit.proto.DualConduitProto.EstablishConduitResponse; +import com.google.devtools.mobileharness.shared.util.comm.dualconduit.proto.DualConduitProto.TeardownConduitResponse; +import io.grpc.Server; +import java.io.IOException; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +@RunWith(JUnit4.class) +public final class ReverseProxiedServerTest { + + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + + @Mock private Server delegate; + @Mock private DualConduitClient client; + + private ReverseProxiedServer proxyServer; + + @Before + public void setUp() { + proxyServer = new ReverseProxiedServer(delegate, client, "my-server", "localhost", "backend"); + } + + @Test + public void start_success() throws Exception { + when(client.establishReverseGrpcConduit(anyString(), anyString(), anyString())) + .thenReturn(EstablishConduitResponse.newBuilder().setConduitId("fake-conduit-id").build()); + when(delegate.start()).thenReturn(delegate); + when(delegate.getPort()).thenReturn(50051); + + var unused = proxyServer.start(); + + verify(delegate).start(); + verify(client).establishReverseGrpcConduit("my-server", "localhost", "backend:50051"); + } + + @Test + public void start_establishConduitFailed_shutdownDelegate() throws Exception { + when(delegate.start()).thenReturn(delegate); + when(delegate.getPort()).thenReturn(50051); + when(client.establishReverseGrpcConduit(anyString(), anyString(), anyString())) + .thenThrow(new RuntimeException("Failed to establish conduit")); + + assertThrows(IOException.class, () -> proxyServer.start()); + + verify(delegate).start(); + verify(delegate).shutdown(); + } + + @Test + public void shutdown_success() throws Exception { + when(client.establishReverseGrpcConduit(anyString(), anyString(), anyString())) + .thenReturn(EstablishConduitResponse.newBuilder().setConduitId("fake-conduit-id").build()); + when(delegate.start()).thenReturn(delegate); + when(delegate.getPort()).thenReturn(50051); + when(client.teardownConduit(anyString())) + .thenReturn(TeardownConduitResponse.getDefaultInstance()); + + var unusedStart = proxyServer.start(); + var unusedShutdown = proxyServer.shutdown(); + + verify(client).teardownConduit("fake-conduit-id"); + verify(delegate).shutdown(); + } + + @Test + public void shutdown_noConduit_onlyShutdownDelegate() { + var unusedShutdown = proxyServer.shutdown(); + + verify(client, never()).teardownConduit(anyString()); + verify(delegate).shutdown(); + } + + @Test + public void shutdownNow_success() throws Exception { + when(client.establishReverseGrpcConduit(anyString(), anyString(), anyString())) + .thenReturn(EstablishConduitResponse.newBuilder().setConduitId("fake-conduit-id").build()); + when(delegate.start()).thenReturn(delegate); + when(delegate.getPort()).thenReturn(50051); + when(client.teardownConduit(anyString())) + .thenReturn(TeardownConduitResponse.getDefaultInstance()); + + var unusedStart = proxyServer.start(); + var unusedShutdownNow = proxyServer.shutdownNow(); + + verify(client).teardownConduit("fake-conduit-id"); + verify(delegate).shutdownNow(); + } +}