mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-31 06:05:44 +00:00
443 lines
15 KiB
Java
443 lines
15 KiB
Java
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
package org.mozilla.gecko;
|
|
|
|
import java.io.BufferedWriter;
|
|
import java.io.File;
|
|
import java.io.FileOutputStream;
|
|
import java.io.FileReader;
|
|
import java.io.FileWriter;
|
|
import java.io.IOException;
|
|
import java.io.PrintWriter;
|
|
import java.io.StringWriter;
|
|
import java.util.Arrays;
|
|
import java.util.UUID;
|
|
|
|
import android.content.ComponentName;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.pm.PackageInfo;
|
|
import android.content.pm.PackageManager;
|
|
import android.net.Uri;
|
|
import android.os.Build;
|
|
import android.os.Bundle;
|
|
import android.os.Process;
|
|
import android.util.Log;
|
|
|
|
class CrashHandler implements Thread.UncaughtExceptionHandler {
|
|
|
|
private static final String LOGTAG = "GeckoCrashHandler";
|
|
private static final Thread MAIN_THREAD = Thread.currentThread();
|
|
private static final String DEFAULT_SERVER_URL =
|
|
"https://crash-reports.mozilla.com/submit?id=%1$s&version=%2$s&buildid=%3$s";
|
|
|
|
// Context for getting device information
|
|
protected final Context appContext;
|
|
// Thread that this handler applies to, or null for a global handler
|
|
protected final Thread handlerThread;
|
|
protected final Thread.UncaughtExceptionHandler systemUncaughtHandler;
|
|
|
|
protected boolean crashing;
|
|
protected boolean unregistered;
|
|
|
|
/**
|
|
* Get the root exception from the 'cause' chain of an exception.
|
|
*
|
|
* @param exc An exception
|
|
* @return The root exception
|
|
*/
|
|
public static Throwable getRootException(Throwable exc) {
|
|
for (Throwable cause = exc; cause != null; cause = cause.getCause()) {
|
|
exc = cause;
|
|
}
|
|
return exc;
|
|
}
|
|
|
|
/**
|
|
* Get the standard stack trace string of an exception.
|
|
*
|
|
* @param exc An exception
|
|
* @return The exception stack trace.
|
|
*/
|
|
public static String getExceptionStackTrace(final Throwable exc) {
|
|
StringWriter sw = new StringWriter();
|
|
PrintWriter pw = new PrintWriter(sw);
|
|
exc.printStackTrace(pw);
|
|
pw.flush();
|
|
return sw.toString();
|
|
}
|
|
|
|
/**
|
|
* Terminate the current process.
|
|
*/
|
|
public static void terminateProcess() {
|
|
Process.killProcess(Process.myPid());
|
|
}
|
|
|
|
/**
|
|
* Create and register a CrashHandler for all threads and thread groups.
|
|
*/
|
|
public CrashHandler() {
|
|
this((Context) null);
|
|
}
|
|
|
|
/**
|
|
* Create and register a CrashHandler for all threads and thread groups.
|
|
*
|
|
* @param appContext A Context for retrieving application information.
|
|
*/
|
|
public CrashHandler(final Context appContext) {
|
|
this.appContext = appContext;
|
|
this.handlerThread = null;
|
|
this.systemUncaughtHandler = Thread.getDefaultUncaughtExceptionHandler();
|
|
Thread.setDefaultUncaughtExceptionHandler(this);
|
|
}
|
|
|
|
/**
|
|
* Create and register a CrashHandler for a particular thread.
|
|
*
|
|
* @param thread A thread to register the CrashHandler
|
|
*/
|
|
public CrashHandler(final Thread thread) {
|
|
this(thread, null);
|
|
}
|
|
|
|
/**
|
|
* Create and register a CrashHandler for a particular thread.
|
|
*
|
|
* @param thread A thread to register the CrashHandler
|
|
* @param appContext A Context for retrieving application information.
|
|
*/
|
|
public CrashHandler(final Thread thread, final Context appContext) {
|
|
this.appContext = appContext;
|
|
this.handlerThread = thread;
|
|
this.systemUncaughtHandler = thread.getUncaughtExceptionHandler();
|
|
thread.setUncaughtExceptionHandler(this);
|
|
}
|
|
|
|
/**
|
|
* Unregister this CrashHandler for exception handling.
|
|
*/
|
|
public void unregister() {
|
|
unregistered = true;
|
|
|
|
// Restore the previous handler if we are still the topmost handler.
|
|
// If not, we are part of a chain of handlers, and we cannot just restore the previous
|
|
// handler, because that would replace whatever handler that's above us in the chain.
|
|
|
|
if (handlerThread != null) {
|
|
if (handlerThread.getUncaughtExceptionHandler() == this) {
|
|
handlerThread.setUncaughtExceptionHandler(systemUncaughtHandler);
|
|
}
|
|
} else {
|
|
if (Thread.getDefaultUncaughtExceptionHandler() == this) {
|
|
Thread.setDefaultUncaughtExceptionHandler(systemUncaughtHandler);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Record an exception stack in logs.
|
|
*
|
|
* @param thread The exception thread
|
|
* @param exc An exception
|
|
*/
|
|
protected void logException(final Thread thread, final Throwable exc) {
|
|
try {
|
|
Log.e(LOGTAG, ">>> REPORTING UNCAUGHT EXCEPTION FROM THREAD "
|
|
+ thread.getId() + " (\"" + thread.getName() + "\")", exc);
|
|
|
|
if (MAIN_THREAD != thread) {
|
|
Log.e(LOGTAG, "Main thread (" + MAIN_THREAD.getId() + ") stack:");
|
|
for (StackTraceElement ste : MAIN_THREAD.getStackTrace()) {
|
|
Log.e(LOGTAG, " " + ste.toString());
|
|
}
|
|
}
|
|
} catch (final Throwable e) {
|
|
// If something throws here, we want to continue to report the exception,
|
|
// so we catch all exceptions and ignore them.
|
|
}
|
|
}
|
|
|
|
private static long getCrashTime() {
|
|
return System.currentTimeMillis() / 1000;
|
|
}
|
|
|
|
private static long getStartupTime() {
|
|
// Process start time is also the proc file modified time.
|
|
final long uptimeMins = (new File("/proc/self/cmdline")).lastModified();
|
|
if (uptimeMins == 0L) {
|
|
return getCrashTime();
|
|
}
|
|
return uptimeMins / 1000;
|
|
}
|
|
|
|
private static String getJavaPackageName() {
|
|
return CrashHandler.class.getPackage().getName();
|
|
}
|
|
|
|
protected String getAppPackageName() {
|
|
final Context context = getAppContext();
|
|
|
|
if (context != null) {
|
|
return context.getPackageName();
|
|
}
|
|
|
|
try {
|
|
// Package name is also the command line string in most cases.
|
|
final FileReader reader = new FileReader("/proc/self/cmdline");
|
|
final char[] buffer = new char[64];
|
|
try {
|
|
if (reader.read(buffer) > 0) {
|
|
// cmdline is delimited by '\0', and we want the first token.
|
|
final int nul = Arrays.asList(buffer).indexOf('\0');
|
|
return (new String(buffer, 0, nul < 0 ? buffer.length : nul)).trim();
|
|
}
|
|
} finally {
|
|
reader.close();
|
|
}
|
|
|
|
} catch (final IOException e) {
|
|
Log.i(LOGTAG, "Error reading package name", e);
|
|
}
|
|
|
|
// Fallback to using CrashHandler's package name.
|
|
return getJavaPackageName();
|
|
}
|
|
|
|
protected Context getAppContext() {
|
|
return appContext;
|
|
}
|
|
|
|
/**
|
|
* Get the crash "extras" to be reported.
|
|
*
|
|
* @param thread The exception thread
|
|
* @param exc An exception
|
|
* @return "Extras" in the from of a Bundle
|
|
*/
|
|
protected Bundle getCrashExtras(final Thread thread, final Throwable exc) {
|
|
final Context context = getAppContext();
|
|
final Bundle extras = new Bundle();
|
|
final String pkgName = getAppPackageName();
|
|
|
|
extras.putString("ProductName", pkgName);
|
|
extras.putLong("CrashTime", getCrashTime());
|
|
extras.putLong("StartupTime", getStartupTime());
|
|
|
|
if (context != null) {
|
|
final PackageManager pkgMgr = context.getPackageManager();
|
|
try {
|
|
final PackageInfo pkgInfo = pkgMgr.getPackageInfo(pkgName, 0);
|
|
extras.putString("Version", pkgInfo.versionName);
|
|
extras.putInt("BuildID", pkgInfo.versionCode);
|
|
extras.putLong("InstallTime", pkgInfo.firstInstallTime / 1000);
|
|
} catch (final PackageManager.NameNotFoundException e) {
|
|
Log.i(LOGTAG, "Error getting package info", e);
|
|
}
|
|
}
|
|
|
|
extras.putString("JavaStackTrace", getExceptionStackTrace(exc));
|
|
return extras;
|
|
}
|
|
|
|
/**
|
|
* Get the crash minidump content to be reported.
|
|
*
|
|
* @param thread The exception thread
|
|
* @param exc An exception
|
|
* @return Minidump content
|
|
*/
|
|
protected byte[] getCrashDump(final Thread thread, final Throwable exc) {
|
|
return new byte[0]; // No minidump.
|
|
}
|
|
|
|
protected static String normalizeUrlString(final String str) {
|
|
if (str == null) {
|
|
return "";
|
|
}
|
|
return Uri.encode(str);
|
|
}
|
|
|
|
/**
|
|
* Get the server URL to send the crash report to.
|
|
*
|
|
* @param extras The crash extras Bundle
|
|
*/
|
|
protected String getServerUrl(final Bundle extras) {
|
|
return String.format(DEFAULT_SERVER_URL,
|
|
normalizeUrlString(extras.getString("ProductID")),
|
|
normalizeUrlString(extras.getString("Version")),
|
|
normalizeUrlString(extras.getString("BuildID")));
|
|
}
|
|
|
|
/**
|
|
* Launch the crash reporter activity that sends the crash report to the server.
|
|
*
|
|
* @param dumpFile Path for the minidump file
|
|
* @param extraFile Path for the crash extra file
|
|
* @return Whether the crash reporter was successfully launched
|
|
*/
|
|
protected boolean launchCrashReporter(final String dumpFile, final String extraFile) {
|
|
try {
|
|
final Context context = getAppContext();
|
|
final String javaPkg = getJavaPackageName();
|
|
final String pkg = getAppPackageName();
|
|
final String component = javaPkg + ".CrashReporter";
|
|
final String action = javaPkg + ".reportCrash";
|
|
final ProcessBuilder pb;
|
|
|
|
if (context != null) {
|
|
final Intent intent = new Intent(action);
|
|
intent.setComponent(new ComponentName(pkg, component));
|
|
intent.putExtra("minidumpPath", dumpFile);
|
|
context.startActivity(intent);
|
|
return true;
|
|
}
|
|
|
|
// Avoid AppConstants dependency for SDK version constants,
|
|
// because CrashHandler could be used outside of Fennec code.
|
|
if (Build.VERSION.SDK_INT < 17) {
|
|
pb = new ProcessBuilder(
|
|
"/system/bin/am", "start",
|
|
"-a", action,
|
|
"-n", pkg + '/' + component,
|
|
"--es", "minidumpPath", dumpFile);
|
|
} else {
|
|
pb = new ProcessBuilder(
|
|
"/system/bin/am", "start",
|
|
"--user", /* USER_CURRENT_OR_SELF */ "-3",
|
|
"-a", action,
|
|
"-n", pkg + '/' + component,
|
|
"--es", "minidumpPath", dumpFile);
|
|
}
|
|
|
|
pb.start().waitFor();
|
|
|
|
} catch (final IOException e) {
|
|
Log.e(LOGTAG, "Error launching crash reporter", e);
|
|
return false;
|
|
|
|
} catch (final InterruptedException e) {
|
|
Log.i(LOGTAG, "Interrupted while waiting to launch crash reporter", e);
|
|
// Fall-through
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Report an exception to Socorro.
|
|
*
|
|
* @param thread The exception thread
|
|
* @param exc An exception
|
|
* @return Whether the exception was successfully reported
|
|
*/
|
|
protected boolean reportException(final Thread thread, final Throwable exc) {
|
|
final Context context = getAppContext();
|
|
final String id = UUID.randomUUID().toString();
|
|
|
|
// Use the cache directory under the app directory to store crash files.
|
|
final File dir;
|
|
if (context != null) {
|
|
dir = context.getCacheDir();
|
|
} else {
|
|
dir = new File("/data/data/" + getAppPackageName() + "/cache");
|
|
}
|
|
|
|
dir.mkdirs();
|
|
if (!dir.exists()) {
|
|
return false;
|
|
}
|
|
|
|
final File dmpFile = new File(dir, id + ".dmp");
|
|
final File extraFile = new File(dir, id + ".extra");
|
|
|
|
try {
|
|
// Write out minidump file as binary.
|
|
|
|
final byte[] minidump = getCrashDump(thread, exc);
|
|
final FileOutputStream dmpStream = new FileOutputStream(dmpFile);
|
|
try {
|
|
dmpStream.write(minidump);
|
|
} finally {
|
|
dmpStream.close();
|
|
}
|
|
|
|
} catch (final IOException e) {
|
|
Log.e(LOGTAG, "Error writing minidump file", e);
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
// Write out crash extra file as text.
|
|
|
|
final Bundle extras = getCrashExtras(thread, exc);
|
|
final String url = getServerUrl(extras);
|
|
extras.putString("ServerURL", url);
|
|
|
|
final BufferedWriter extraWriter = new BufferedWriter(new FileWriter(extraFile));
|
|
try {
|
|
for (String key : extras.keySet()) {
|
|
// Each extra line is in the format, key=value, with newlines escaped.
|
|
extraWriter.write(key);
|
|
extraWriter.write('=');
|
|
extraWriter.write(String.valueOf(extras.get(key)).replace("\n", "\\n"));
|
|
extraWriter.write('\n');
|
|
}
|
|
} finally {
|
|
extraWriter.close();
|
|
}
|
|
|
|
} catch (final IOException e) {
|
|
Log.e(LOGTAG, "Error writing extra file", e);
|
|
return false;
|
|
}
|
|
|
|
return launchCrashReporter(dmpFile.getAbsolutePath(), extraFile.getAbsolutePath());
|
|
}
|
|
|
|
/**
|
|
* Implements the default behavior for handling uncaught exceptions.
|
|
*
|
|
* @param thread The exception thread
|
|
* @param exc An uncaught exception
|
|
*/
|
|
@Override
|
|
public void uncaughtException(Thread thread, Throwable exc) {
|
|
if (this.crashing) {
|
|
// Prevent possible infinite recusions.
|
|
return;
|
|
}
|
|
|
|
if (thread == null) {
|
|
// Gecko may pass in null for thread to denote the current thread.
|
|
thread = Thread.currentThread();
|
|
}
|
|
|
|
try {
|
|
if (!this.unregistered) {
|
|
// Only process crash ourselves if we have not been unregistered.
|
|
|
|
this.crashing = true;
|
|
exc = getRootException(exc);
|
|
logException(thread, exc);
|
|
|
|
if (reportException(thread, exc)) {
|
|
// Reporting succeeded; we can terminate our process now.
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (systemUncaughtHandler != null) {
|
|
// Follow the chain of uncaught handlers.
|
|
systemUncaughtHandler.uncaughtException(thread, exc);
|
|
}
|
|
} finally {
|
|
terminateProcess();
|
|
}
|
|
}
|
|
}
|