1 /*
2  * Copyright (C) 2023 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 
17 package com.android.bedstead.nene.logcat;
18 
19 import android.util.Log;
20 
21 import com.android.bedstead.nene.TestApis;
22 import com.android.bedstead.nene.annotations.Experimental;
23 import com.android.bedstead.nene.exceptions.AdbException;
24 import com.android.bedstead.nene.exceptions.NeneException;
25 import com.android.bedstead.nene.utils.Retry;
26 import com.android.bedstead.nene.utils.ShellCommand;
27 import com.android.bedstead.nene.utils.ShellCommandUtils;
28 
29 import java.io.IOException;
30 import java.time.Duration;
31 import java.util.ArrayList;
32 import java.util.Arrays;
33 import java.util.Comparator;
34 import java.util.Iterator;
35 import java.util.List;
36 import java.util.NoSuchElementException;
37 import java.util.function.Predicate;
38 import java.util.stream.Collectors;
39 
40 /**
41  * TestApis related to logcat.
42  */
43 public final class Logcat {
44 
45     private static final String LOG_TAG = "Nene.Logcat";
46 
47     public static final Logcat sInstance = new Logcat();
48 
Logcat()49     private Logcat() {
50 
51     }
52 
53     /** Clear the logcat buffer. */
54     @Experimental
clear()55     public void clear() {
56         try {
57             String unused = Retry.logic(() ->
58                             ShellCommand.builder("logcat")
59                                     .addOperand("-c")
60                                     .validate(String::isEmpty)
61                                     .executeOrThrowNeneException("Error clearing logcat buffer"))
62                     .timeout(Duration.ofSeconds(10))
63                     .run();
64         } catch (Throwable e) {
65             // Clearing is best effort - don't disrupt the test because we can't
66             Log.e(LOG_TAG, "Error clearing logcat", e);
67         }
68     }
69 
70     /**
71      * Get an instant dump from logcat, filtered by {@code lineFilter}.
72      */
dump(Predicate<String> lineFilter)73     public String dump(Predicate<String> lineFilter) {
74         try (ShellCommandUtils.StreamingShellOutput sso = dump()){
75             String log = sso.stream().filter(lineFilter)
76                     .collect(Collectors.joining("\n"));
77 
78             // We only take the last 500 characters - this can be relaxed once we properly block
79             // out the start and end of the time we care about
80             return log.substring(Math.max(0, log.length() - 500));
81         } catch (IOException e) {
82             throw new NeneException("Error dumping logcat", e);
83         }
84     }
85 
86     /**
87      * Get an instant dump from logcat.
88      *
89      * <p>Note that this might include a lot of data which could cause memory issues if stored
90      * in a String.
91      *
92      * <p>Make sure you close the {@link ShellCommandUtils.StreamingShellOutput} after reading
93      */
dump()94     public ShellCommandUtils.StreamingShellOutput dump() {
95         try {
96             return ShellCommandUtils.executeCommandForStream(
97                     "logcat -d", /* /* stdInBytes= */ null
98             );
99         } catch (AdbException e) {
100             throw new NeneException("Error dumping logcat", e);
101         }
102     }
103 
104     /**
105      * Find a system server exception in logcat matching the passed in {@link Throwable}.
106      *
107      * <p>If there is any problem finding a matching exception, or if the exception is not found,
108      * then {@code null} will be returned.
109      */
findSystemServerException(Throwable t)110     public SystemServerException findSystemServerException(Throwable t) {
111         List<SystemServerException> exceptions = findSystemServerExceptions(t);
112         if (exceptions.isEmpty()) {
113             return null;
114         }
115         return exceptions.get(exceptions.size() - 1);
116     }
117 
118     /**
119      * Get the most recent log from logcat matching the {@code lineFilter}.
120      */
recent(Predicate<String> lineFilter)121     public String recent(Predicate<String> lineFilter) {
122         String[] logs = dump(lineFilter).split("\n");
123         return logs[logs.length - 1];
124     }
125 
findSystemServerExceptions(Throwable t)126     private List<SystemServerException> findSystemServerExceptions(Throwable t) {
127         List<SystemServerException> exceptions = new ArrayList<>();
128 
129         try (ShellCommandUtils.StreamingShellOutput sso = dump()){
130             Iterator<String> lines = sso.stream().iterator();
131 
132             while (true) {
133                 String nextline = lines.next();
134                 if (nextline.contains(
135                         "Caught a RuntimeException from the binder stub implementation.")) {
136 
137                     String binderPrefix = lines.next();
138 
139                     // First split to remove the Binder prefix
140                     String exceptionTitle = binderPrefix.split(": ", 2)[1];
141                     String[] exceptionTitlePaths = exceptionTitle.split(": ", 2);
142                     String exceptionClass = exceptionTitlePaths[0];
143                     String exceptionMessage = exceptionTitlePaths[1];
144 
145                     if (exceptionClass.equals(t.getClass().getName())) {
146                         if (exceptionMessage.equals(t.getMessage())) {
147                             List<String> traceLines = new ArrayList<>();
148 
149                             while (true) {
150                                 String traceLine = lines.next();
151 
152                                 if (traceLine.contains("W Binder  : ")) {
153                                     traceLines.add(traceLine.split(
154                                             "W Binder  : ", 2)[1].strip());
155                                 } else {
156                                     // This means we will miss if two traces are right after each
157                                     // other as we lose this line - but probably not a huge deal...
158                                     break;
159                                 }
160                             }
161 
162                             exceptions.add(extractStackTraceFromStrings(
163                                     exceptionClass, exceptionMessage, traceLines, t));
164                         }
165                     }
166                 }
167             }
168         } catch (NoSuchElementException e) {
169             // Finished reading
170             return exceptions;
171         } catch (RuntimeException | IOException e) {
172             // Any issues we will just return nothing so we don't hide a real exception
173             Log.e(LOG_TAG, "Error finding system server exception", e);
174             return exceptions;
175         }
176     }
177 
extractStackTraceFromStrings( String exceptionClass, String exceptionMessage, List<String> traceLines, Throwable cause)178     private SystemServerException extractStackTraceFromStrings(
179             String exceptionClass, String exceptionMessage, List<String> traceLines,
180             Throwable cause) {
181             StackTraceElement[] traceElements =
182                     traceLines.stream().map(
183                             this::extractStackTraceElement).toArray(StackTraceElement[]::new);
184             return new SystemServerException(
185                     exceptionClass, exceptionMessage, traceElements, cause);
186     }
187 
extractStackTraceElement(String line)188     private StackTraceElement extractStackTraceElement(String line) {
189         String element = line.split("at ", 2)[1];
190         String[] elementParts = element.split("\\(");
191         String className = elementParts[0];
192         int methodNameSeparator = className.lastIndexOf(".");
193         if (methodNameSeparator == -1) {
194             throw new IllegalStateException("Could not parse " + line);
195         }
196         String methodName = className.substring(methodNameSeparator + 1);
197         className = className.substring(0, methodNameSeparator);
198 
199         String[] fileParts =
200                 elementParts[1].substring(
201                         0, elementParts[1].length() - 1).split(":", 2);
202 
203         return new StackTraceElement(
204                 className, methodName, fileParts[0], Integer.parseInt(fileParts[1]));
205     }
206 
207     /**
208      * Begin listening for a particular entry in logcat.
209      *
210      * <p>Example usage:
211      *
212      * try (BlockingLogcatListener l = TestApis.logcat().listen(l -> l.contains("line")) {
213      *     // Some code which will cause line to appear in the output
214      * } // this will block until line appears
215      */
216     @Experimental
listen(Predicate<String> lineFilter)217     public BlockingLogcatListener listen(Predicate<String> lineFilter) {
218         // TODO: Replace this with actually filtering on an ongoing basis - so we don't clear
219         // the logcat and so it can run longer than a single logcat buffer
220         TestApis.logcat().clear();
221 
222         return new BlockingLogcatListener(lineFilter);
223     }
224 }
225