1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.media.audiotestharness.server;
17 
18 import com.android.media.audiotestharness.proto.AudioTestHarnessGrpc;
19 import com.android.media.audiotestharness.server.utility.PortUtility;
20 
21 import com.google.common.annotations.VisibleForTesting;
22 import com.google.common.base.Preconditions;
23 import com.google.inject.ConfigurationException;
24 import com.google.inject.Guice;
25 import com.google.inject.Injector;
26 
27 import io.grpc.Server;
28 import io.grpc.ServerBuilder;
29 
30 import java.io.IOException;
31 import java.util.concurrent.Executor;
32 import java.util.concurrent.ExecutorService;
33 import java.util.concurrent.Executors;
34 import java.util.logging.Level;
35 import java.util.logging.Logger;
36 
37 /**
38  * gRPC Server to the Audio Test Harness Infrastructure.
39  *
40  * <p>This class is designed in such a way that the spin up and tear down of the gRPC server,
41  * including all dependencies are abstracted away from any users of Audio Test Harness. It is
42  * possible to use the service implementation directly and customize the deployment of the service,
43  * however, this is meant as an easy alternative for users who just want a working system and don't
44  * care how it works under the hood.
45  */
46 public final class AudioTestHarnessGrpcServer implements AutoCloseable {
47 
48     private static final int TESTING_PORT = 8080;
49 
50     private static final Logger LOGGER =
51             Logger.getLogger(AudioTestHarnessGrpcServer.class.getName());
52 
53     /** Number of threads used by the {@link #mExecutor} for task execution. */
54     private static final int THREAD_COUNT = 16;
55 
56     /** The port on which the gRPC Server should be executed on. */
57     private final int mPort;
58 
59     /**
60      * {@link Executor} used for background task execution. These tasks can be the handling of gRPC
61      * calls or other background tasks in the system such as the publishing of audio sample data by
62      * a {@link com.android.media.audiotestharness.server.core.AudioCapturer}.
63      *
64      * <p>This {@link ExecutorService} utilizes a fixed-thread pool with the number of threads set
65      * by the {@link #THREAD_COUNT} constant.
66      */
67     private final ExecutorService mExecutor;
68 
69     /** {@link Injector} used for dependency management and injection. */
70     private final Injector mInjector;
71 
72     /** Underlying {@link Server} that manages gRPC calls. */
73     private Server mServer;
74 
AudioTestHarnessGrpcServer(int port, ExecutorService executor, Injector injector)75     private AudioTestHarnessGrpcServer(int port, ExecutorService executor, Injector injector) {
76         LOGGER.finest(String.format("new AudioTestHarnessGrpcServer on Port (%d)", port));
77 
78         this.mPort = port;
79         this.mExecutor = executor;
80         this.mInjector = injector;
81     }
82 
83     /**
84      * Creates a new {@link AudioTestHarnessGrpcServer} with the provided port, {@link
85      * ExecutorService}, and {@link Injector}.
86      *
87      * <p>This method is primarily available for unit testing. In production use, the {@link
88      * #createOnPort(int)} or {@link #createWithDefault()} methods should be used instead.
89      *
90      * @throws NullPointerException if either the executor or injector is null.
91      */
92     @VisibleForTesting
create( int port, ExecutorService executor, Injector injector)93     static AudioTestHarnessGrpcServer create(
94             int port, ExecutorService executor, Injector injector) {
95         return new AudioTestHarnessGrpcServer(
96                 port,
97                 Preconditions.checkNotNull(executor, "Executor cannot be null."),
98                 Preconditions.checkNotNull(injector, "Injector cannot be null."));
99     }
100 
101     /** Creates a new {@link AudioTestHarnessGrpcServer} with the provided port. */
createOnPort(int port)102     public static AudioTestHarnessGrpcServer createOnPort(int port) {
103         LOGGER.finest(
104                 String.format(
105                         "Using default Fixed Thread Pool Executor with %d threads and default"
106                                 + " Injector using %s",
107                         THREAD_COUNT, AudioTestHarnessServerModule.class.getName()));
108 
109         ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
110         Injector injector = Guice.createInjector(AudioTestHarnessServerModule.create(executor));
111 
112         return create(port, executor, injector);
113     }
114 
115     /**
116      * Creates a new {@link AudioTestHarnessGrpcServer} on an available port in the dynamic port
117      * range.
118      */
createWithDefault()119     public static AudioTestHarnessGrpcServer createWithDefault() {
120         return createOnPort(PortUtility.nextAvailablePort());
121     }
122 
main(String[] args)123     public static void main(String[] args) {
124         try (AudioTestHarnessGrpcServer server = createOnPort(TESTING_PORT)) {
125             server.open();
126 
127             // Ensure that resources are cleanly stopped upon JVM shutdown.
128             Runtime.getRuntime().addShutdownHook(new Thread(server::close));
129 
130             // Wait for server execution to complete. So that CTRL + C or some other mechanism must
131             // be used to kill the server.
132             server.mServer.awaitTermination();
133 
134         } catch (Exception e) {
135             LOGGER.log(Level.SEVERE, "Unable to start the Audio Test Harness gRPC Server.", e);
136         }
137     }
138 
139     /**
140      * Starts the Audio Test Harness gRPC Server listening on {@link #mPort}.
141      *
142      * @throws IOException if any errors occur while provisioning or starting the server.
143      */
open()144     public void open() throws IOException {
145         Preconditions.checkState(mServer == null, "Server has already been opened.");
146 
147         ServerBuilder<?> serverBuilder = ServerBuilder.forPort(mPort).executor(mExecutor);
148 
149         try {
150             serverBuilder.addService(
151                     mInjector.getInstance(AudioTestHarnessGrpc.AudioTestHarnessImplBase.class));
152         } catch (ConfigurationException ce) {
153             throw new IOException(
154                     "Unable to create new Audio Test Harness gRPC Server, failed to resolve"
155                             + " required dependency.",
156                     ce);
157         }
158 
159         mServer = serverBuilder.build();
160         LOGGER.fine("Successfully created Audio Test Harness gRPC Server.");
161 
162         mServer.start();
163         LOGGER.info(
164                 String.format(
165                         "Started Audio Test Harness gRPC Server Listening on Port %d", mPort));
166     }
167 
168     /**
169      * Stops the Audio Test Harness gRPC Server immediately and closes any underlying resources
170      * being used by the server.
171      */
172     @Override
close()173     public void close() {
174 
175         LOGGER.info("Stopping Audio Test Harness gRPC Server");
176         if (mServer != null) {
177             mServer.shutdownNow();
178         } else {
179             LOGGER.warning(
180                     "mServer is null indicating that the Audio Test Harness gRPC Server was never"
181                             + " started.");
182         }
183 
184         mExecutor.shutdownNow();
185     }
186 
getPort()187     public int getPort() {
188         return mPort;
189     }
190 }
191