• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 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.google.asuite.clearcut.junit.listener;
17 
18 import com.android.asuite.clearcut.Clientanalytics.ClientInfo;
19 import com.android.asuite.clearcut.Clientanalytics.LogEvent;
20 import com.android.asuite.clearcut.Clientanalytics.LogRequest;
21 import com.android.asuite.clearcut.Clientanalytics.LogResponse;
22 import com.android.asuite.clearcut.Common.UserType;
23 
24 import java.io.BufferedReader;
25 import java.io.ByteArrayOutputStream;
26 import java.io.Closeable;
27 import java.io.File;
28 import java.io.IOException;
29 import java.io.InputStream;
30 import java.io.InputStreamReader;
31 import java.io.OutputStream;
32 import java.net.HttpURLConnection;
33 import java.net.URI;
34 import java.net.URISyntaxException;
35 import java.net.URL;
36 import java.nio.charset.StandardCharsets;
37 import java.nio.file.Files;
38 import java.nio.file.StandardOpenOption;
39 import java.time.Duration;
40 import java.util.Objects;
41 import java.util.Optional;
42 import java.util.UUID;
43 import java.util.concurrent.CompletableFuture;
44 import java.util.concurrent.ExecutionException;
45 import java.util.stream.Collectors;
46 import java.util.zip.GZIPOutputStream;
47 
48 /** Client that allows reporting usage metrics to clearcut. */
49 public class Client {
50 
51     public static final String DISABLE_CLEARCUT_KEY = "DISABLE_CLEARCUT";
52     private static final String CLEARCUT_SUB_TOOL_NAME = "CLEARCUT_SUB_TOOL_NAME";
53 
54     private static final String CLEARCUT_PROD_URL = "https://play.googleapis.com/log";
55     private static final int CLIENT_TYPE = 1;
56     private static final int INTERNAL_LOG_SOURCE = 971;
57     private static final int EXTERNAL_LOG_SOURCE = 934;
58     private File mCachedUuidFile = new File(System.getProperty("user.home"), ".clearcut_listener");
59     private String mRunId;
60     private long mSessionStartTime = 0L;
61     private final int mLogSource;
62     private final String mUrl;
63     private final UserType mUserType;
64     private final String mToolName;
65     private final String mSubToolName;
66     private final String mUser;
67     private final boolean mIsGoogle;
68 
69     // Whether the clearcut client should be a noop
70     private boolean mDisabled = false;
71 
Client(String toolName, String subToolName)72     public Client(String toolName, String subToolName) {
73         this(null, toolName, subToolName);
74         Runtime.getRuntime().addShutdownHook(new Thread(Client.this::stop));
75     }
76 
77     /**
78      * Create Client with customized posting URL and forcing whether it's internal or external user.
79      */
Client(String url, String toolName, String subToolName)80     protected Client(String url, String toolName, String subToolName) {
81         mDisabled = isClearcutDisabled();
82         Optional<String> email = EnvironmentInformation.getGitEmail();
83         Optional<String> googleUser = EnvironmentInformation.getGitUserIfGoogleEmail(email);
84         mIsGoogle = EnvironmentInformation.isGoogleDomain() ||
85                 googleUser.isPresent();
86         Optional<String> username = EnvironmentInformation.executeCommand("whoami");
87 
88         // We still have to set the 'final' variable so go through the assignments before returning
89         if (!mDisabled && isGoogleUser()) {
90             mLogSource = INTERNAL_LOG_SOURCE;
91             mUserType = UserType.GOOGLE;
92             mUser = googleUser.orElse(username.orElse(""));
93         } else {
94             mLogSource = EXTERNAL_LOG_SOURCE;
95             mUserType = UserType.EXTERNAL;
96             mUser = UUID5.uuidOf(UUID5.NAMESPACE_DNS, email.orElse(username.orElse(""))).toString();
97         }
98         mUrl = Objects.requireNonNullElse(url, CLEARCUT_PROD_URL);
99         mToolName = toolName;
100         mRunId = UUID.randomUUID().toString();
101         if (subToolName != null && subToolName.isEmpty() && System.getenv(CLEARCUT_SUB_TOOL_NAME) != null) {
102             mSubToolName = System.getenv(CLEARCUT_SUB_TOOL_NAME);
103         } else {
104             mSubToolName = subToolName;
105         }
106 
107         if (mDisabled) {
108             return;
109         }
110 
111         // Print the notice
112         System.out.println(NoticeMessageUtil.getNoticeMessage(mUserType));
113     }
114 
disable()115     protected void disable(){
116         mDisabled = true;
117     }
118 
isGoogleUser()119     boolean isGoogleUser() {
120         return mIsGoogle;
121     }
122 
123     /** Send the first event to notify that Tradefed was started. */
notifyTradefedStartEvent()124     public void notifyTradefedStartEvent() {
125         if (mDisabled) {
126             return;
127         }
128         mSessionStartTime = System.nanoTime();
129         long eventTimeMs = System.currentTimeMillis();
130         CompletableFuture.supplyAsync(() -> createAndSendStartEvent(eventTimeMs));
131     }
132 
createAndSendStartEvent(long eventTimeMs)133     private boolean createAndSendStartEvent(long eventTimeMs) {
134         LogEvent.Builder logEvent = LogEvent.newBuilder();
135         logEvent.setEventTimeMs(eventTimeMs);
136         logEvent.setSourceExtension(
137                 ClearcutEventHelper.createStartEvent(
138                         mUser, mRunId, mUserType, mToolName, mSubToolName));
139         LogRequest.Builder request = createBaseLogRequest();
140         request.addLogEvent(logEvent);
141         sendEvent(request.build());
142         return true;
143     }
144 
notifyTradefedFinishedEvent()145     public void notifyTradefedFinishedEvent() {
146         if (mDisabled) {
147             return;
148         }
149         CompletableFuture.supplyAsync(() -> createAndSendFinishedEvent());
150     }
151 
152     /** Send the last event to notify that Tradefed is done. */
createAndSendFinishedEvent()153     public boolean createAndSendFinishedEvent() {
154         Duration duration = java.time.Duration.ofNanos(System.nanoTime() - mSessionStartTime);
155         LogEvent.Builder logEvent = LogEvent.newBuilder();
156         logEvent.setEventTimeMs(System.currentTimeMillis());
157         logEvent.setSourceExtension(
158                 ClearcutEventHelper.createFinishedEvent(
159                         mUser, mRunId, mUserType, mToolName, mSubToolName, duration));
160         LogRequest.Builder request = createBaseLogRequest();
161         request.addLogEvent(logEvent);
162         sendEvent(request.build());
163         return true;
164     }
165 
166     /** Stop the periodic sending of clearcut events */
stop()167     public void stop() {
168         notifyTradefedFinishedEvent();
169     }
170 
171     /** Returns True if clearcut is disabled, False otherwise. */
isClearcutDisabled()172     public boolean isClearcutDisabled() {
173         return "1".equals(System.getenv(DISABLE_CLEARCUT_KEY));
174     }
175 
createBaseLogRequest()176     private LogRequest.Builder createBaseLogRequest() {
177         LogRequest.Builder request = LogRequest.newBuilder();
178         request.setLogSource(mLogSource);
179         request.setClientInfo(ClientInfo.newBuilder().setClientType(CLIENT_TYPE));
180         return request;
181     }
182 
sendEvent(LogRequest request)183     private void sendEvent(LogRequest request) {
184         CompletableFuture<Boolean> future = CompletableFuture.supplyAsync(() -> sendToClearcut(request));
185         try {
186             future.get();
187         } catch (InterruptedException | ExecutionException e) {
188             logError(e);
189         }
190     }
191 
192     /** Send one event to the configured server. */
sendToClearcut(LogRequest event)193     private boolean sendToClearcut(LogRequest event) {
194         InputStream inputStream = null;
195         InputStream errorStream = null;
196         OutputStream outputStream = null;
197         try {
198             HttpURLConnection connection = createConnection(new URI(mUrl).toURL(), "POST", null);
199             connection.setRequestProperty("Content-Encoding", "gzip");
200             connection.setRequestProperty("Content-Type", "application/x-gzip");
201             outputStream = connection.getOutputStream();
202             outputStream.write(gzipCompress(event.toByteArray()));
203             outputStream.flush();
204 
205             inputStream = connection.getInputStream();
206             LogResponse response = LogResponse.parseFrom(inputStream);
207 
208             errorStream = connection.getErrorStream();
209             if (errorStream != null) {
210                 String message =  readStream(errorStream);
211                 System.out.println("Error posting clearcut event: " + message + " LogResponse: " + response);
212             }
213         } catch (IOException | URISyntaxException e) {
214             logError(e);
215         } catch (NoSuchMethodError e) {
216             if (e.getMessage().contains("com.google.protobuf.Descriptors$Descriptor com.google.protobuf.Any.getDescriptor()")) {
217                 String message =
218                         "In order for the ClearcutListener to operate it must use protobuf-full to be able to convert messages to json.\n"
219                                 + "Android typically uses protobuf-lite."
220                                 + "If you're seeing this in a gradle build, adding `testImplementation(project(\":RobolectricLib\"))` to the start of "
221                                 + "your dependency section should be sufficient, if not (due to how gradle calculates deps), add this dep: "
222                                 + "`testImplementation(libs.protobuf.java)` to the top of your dependencies";
223                 throw new RuntimeException(message, e);
224             } else {
225                 logError(e);
226                 throw e;
227             }
228         } catch (Throwable t) {
229             logError(t);
230             throw t;
231         } finally {
232             closeQuietly(outputStream);
233             closeQuietly(inputStream);
234             closeQuietly(errorStream);
235         }
236         return true;
237     }
238 
closeQuietly(Closeable c)239     private void closeQuietly(Closeable c) {
240         try {
241             if (c != null) {
242                 c.close();
243             }
244         } catch (IOException ex) {
245             // Intentional No-Op
246         }
247     }
248 
logError(Throwable t)249     private void logError(Throwable t) {
250         System.out.println(t);
251         t.printStackTrace(System.out);
252     }
253 
readFromFile(File file)254     protected static String readFromFile(File file) throws IOException {
255         return Files.readString(file.toPath());
256     }
257 
writeToFile(String content, File file)258     protected static void writeToFile(String content, File file) throws IOException {
259         Files.writeString(file.toPath(), content, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
260     }
261 
readStream(InputStream is)262     protected static String readStream(InputStream is) throws IOException {
263         try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
264             return reader.lines().collect(Collectors.joining(System.lineSeparator()));
265         }
266     }
267 
createConnection(URL url, String method, String contentType)268     private static HttpURLConnection createConnection(URL url, String method, String contentType)
269             throws IOException {
270         HttpURLConnection connection = (HttpURLConnection) url.openConnection();
271         connection.setRequestMethod(method);
272         if (contentType != null) {
273             connection.setRequestProperty("Content-Type", contentType);
274         }
275         connection.setDoInput(true);
276         connection.setDoOutput(true);
277         connection.setConnectTimeout(60 * 1000);  // timeout for establishing the connection
278         connection.setReadTimeout(60 * 1000);  // timeout for receiving a read() response
279         connection.setRequestProperty("User-Agent",
280                 String.format("%s/%s", "TradeFederation_like_ClearcutJunitListener", "1.0"));
281 
282         return connection;
283     }
284 
gzipCompress(byte[] data)285     private static byte[] gzipCompress(byte[] data) throws IOException {
286         ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
287         try (GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream)) {
288             gzipOutputStream.write(data);
289         }
290         return outputStream.toByteArray();
291     }
292 }
293