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