blob: 4ecbc446c9dd3069c73edc34ca67b96a01bc72ab [file] [log] [blame]
// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.crash;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.Log;
import org.chromium.base.PiiElider;
import org.chromium.components.minidump_uploader.CrashFileManager;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
/**
* Extracts the recent logcat output from an Android device, elides PII sensitive info from it,
* prepends the logcat data to the caller-provided minidump file.
*
* Elided information includes: Emails, IP address, MAC address, URL/domains as well as Javascript
* console messages.
*/
public class LogcatCrashExtractor {
private static final String TAG = "LogcatCrashExtractor";
private static final long HALF_SECOND = 500;
protected static final int LOGCAT_SIZE = 256; // Number of lines.
@VisibleForTesting
protected static final String BEGIN_MICRODUMP = "-----BEGIN BREAKPAD MICRODUMP-----";
@VisibleForTesting
protected static final String END_MICRODUMP = "-----END BREAKPAD MICRODUMP-----";
@VisibleForTesting
protected static final String SNIPPED_MICRODUMP =
"-----SNIPPED OUT BREAKPAD MICRODUMP FOR THIS CRASH-----";
/** @param minidump The minidump file that needs logcat output to be attached. */
public File attachLogcatToMinidump(File minidump, CrashFileManager fileManager) {
Log.i(TAG, "Trying to extract logcat for minidump %s.", minidump.getName());
File fileToUpload = minidump;
try {
List<String> logcat = getElidedLogcat();
fileToUpload = new MinidumpLogcatPrepender(fileManager, minidump, logcat).run();
Log.i(TAG, "Succeeded extracting logcat to %s.", fileToUpload.getName());
} catch (IOException | InterruptedException e) {
Log.w(TAG, e.toString());
}
return fileToUpload;
}
private List<String> getElidedLogcat() throws IOException, InterruptedException {
List<String> rawLogcat = getLogcat();
return Collections.unmodifiableList(elideLogcat(rawLogcat));
}
@VisibleForTesting
protected List<String> getLogcat() throws IOException, InterruptedException {
// Grab the last lines of the logcat output, with a generous buffer to compensate for any
// microdumps that might be in the logcat output, since microdumps are stripped in the
// extraction code. Note that the repeated check of the process exit value is to account for
// the fact that the process might not finish immediately. And, it's not appropriate to
// call p.waitFor(), because this call will block *forever* if the process's output buffer
// fills up.
Process p = Runtime.getRuntime().exec("logcat -d");
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
LinkedList<String> rawLogcat = new LinkedList<>();
Integer exitValue = null;
try {
while (exitValue == null) {
String logLn;
while ((logLn = reader.readLine()) != null) {
rawLogcat.add(logLn);
if (rawLogcat.size() > LOGCAT_SIZE * 4) {
rawLogcat.removeFirst();
}
}
try {
exitValue = p.exitValue();
} catch (IllegalThreadStateException itse) {
Thread.sleep(HALF_SECOND);
}
}
} finally {
reader.close();
}
if (exitValue != 0) {
String msg = "Logcat failed: " + exitValue;
Log.w(TAG, msg);
throw new IOException(msg);
}
return trimLogcat(rawLogcat, LOGCAT_SIZE);
}
/**
* Extracts microdump-free logcat for more informative crash reports. Returns the most recent
* lines that are likely to be relevant to the crash, which are either the lines leading up to a
* microdump if a microdump is present, or just the final lines of the logcat if no microdump is
* present.
*
* @param rawLogcat The last lines of the raw logcat file, with sufficient history to allow a
* sufficient history even after trimming.
* @param maxLines The maximum number of lines logcat extracts from minidump.
*
* @return Logcat up to specified length as a list of strings.
*/
@VisibleForTesting
protected static List<String> trimLogcat(List<String> rawLogcat, int maxLines) {
// Trim off the last microdump, and anything after it.
for (int i = rawLogcat.size() - 1; i >= 0; i--) {
if (rawLogcat.get(i).contains(BEGIN_MICRODUMP)) {
rawLogcat = rawLogcat.subList(0, i);
rawLogcat.add(SNIPPED_MICRODUMP);
break;
}
}
// Trim down the remainder to only contain the most recent lines. Thus, if the original
// input contained a microdump, the result contains the most recent lines before the
// microdump, which are most likely to be relevant to the crash. If there is no microdump
// in the raw logcat, then just hope that the last lines in the dump are relevant.
if (rawLogcat.size() > maxLines) {
rawLogcat = rawLogcat.subList(rawLogcat.size() - maxLines, rawLogcat.size());
}
return rawLogcat;
}
@VisibleForTesting
protected static List<String> elideLogcat(List<String> rawLogcat) {
List<String> elided = new ArrayList<String>(rawLogcat.size());
for (String ln : rawLogcat) {
ln = PiiElider.elideEmail(ln);
ln = PiiElider.elideUrl(ln);
ln = PiiElider.elideIp(ln);
ln = PiiElider.elideMac(ln);
ln = PiiElider.elideConsole(ln);
elided.add(ln);
}
return elided;
}
}