michael@0: /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko; michael@0: michael@0: import java.io.BufferedOutputStream; michael@0: import java.io.BufferedReader; michael@0: import java.io.File; michael@0: import java.io.FileInputStream; michael@0: import java.io.FileOutputStream; michael@0: import java.io.FileReader; michael@0: import java.io.IOException; michael@0: import java.io.InputStreamReader; michael@0: import java.io.OutputStream; michael@0: import java.io.Reader; michael@0: import java.util.UUID; michael@0: import java.util.regex.Pattern; michael@0: michael@0: import org.json.JSONObject; michael@0: import org.mozilla.gecko.util.ThreadUtils; michael@0: michael@0: import android.content.BroadcastReceiver; michael@0: import android.content.Context; michael@0: import android.content.Intent; michael@0: import android.content.IntentFilter; michael@0: import android.os.Handler; michael@0: import android.os.Looper; michael@0: import android.util.Log; michael@0: michael@0: public final class ANRReporter extends BroadcastReceiver michael@0: { michael@0: private static final boolean DEBUG = false; michael@0: private static final String LOGTAG = "GeckoANRReporter"; michael@0: michael@0: private static final String ANR_ACTION = "android.intent.action.ANR"; michael@0: // Number of lines to search traces.txt to decide whether it's a Gecko ANR michael@0: private static final int LINES_TO_IDENTIFY_TRACES = 10; michael@0: // ANRs may happen because of memory pressure, michael@0: // so don't use up too much memory here michael@0: // Size of buffer to hold one line of text michael@0: private static final int TRACES_LINE_SIZE = 100; michael@0: // Size of block to use when processing traces.txt michael@0: private static final int TRACES_BLOCK_SIZE = 2000; michael@0: private static final String TRACES_CHARSET = "utf-8"; michael@0: private static final String PING_CHARSET = "utf-8"; michael@0: michael@0: private static final ANRReporter sInstance = new ANRReporter(); michael@0: private static int sRegisteredCount; michael@0: private Handler mHandler; michael@0: private volatile boolean mPendingANR; michael@0: michael@0: private static native boolean requestNativeStack(boolean unwind); michael@0: private static native String getNativeStack(); michael@0: private static native void releaseNativeStack(); michael@0: michael@0: public static void register(Context context) { michael@0: if (sRegisteredCount++ != 0) { michael@0: // Already registered michael@0: return; michael@0: } michael@0: sInstance.start(context); michael@0: } michael@0: michael@0: public static void unregister() { michael@0: if (sRegisteredCount == 0) { michael@0: Log.w(LOGTAG, "register/unregister mismatch"); michael@0: return; michael@0: } michael@0: if (--sRegisteredCount != 0) { michael@0: // Should still be registered michael@0: return; michael@0: } michael@0: sInstance.stop(); michael@0: } michael@0: michael@0: private void start(final Context context) { michael@0: michael@0: Thread receiverThread = new Thread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: Looper.prepare(); michael@0: synchronized (ANRReporter.this) { michael@0: mHandler = new Handler(); michael@0: ANRReporter.this.notify(); michael@0: } michael@0: if (DEBUG) { michael@0: Log.d(LOGTAG, "registering receiver"); michael@0: } michael@0: context.registerReceiver(ANRReporter.this, michael@0: new IntentFilter(ANR_ACTION), michael@0: null, michael@0: mHandler); michael@0: Looper.loop(); michael@0: michael@0: if (DEBUG) { michael@0: Log.d(LOGTAG, "unregistering receiver"); michael@0: } michael@0: context.unregisterReceiver(ANRReporter.this); michael@0: mHandler = null; michael@0: } michael@0: }, LOGTAG); michael@0: michael@0: receiverThread.setDaemon(true); michael@0: receiverThread.start(); michael@0: } michael@0: michael@0: private void stop() { michael@0: synchronized (this) { michael@0: while (mHandler == null) { michael@0: try { michael@0: wait(1000); michael@0: if (mHandler == null) { michael@0: // We timed out; just give up. The process is probably michael@0: // quitting anyways, so we let the OS do the clean up michael@0: Log.w(LOGTAG, "timed out waiting for handler"); michael@0: return; michael@0: } michael@0: } catch (InterruptedException e) { michael@0: } michael@0: } michael@0: } michael@0: Looper looper = mHandler.getLooper(); michael@0: looper.quit(); michael@0: try { michael@0: looper.getThread().join(); michael@0: } catch (InterruptedException e) { michael@0: } michael@0: } michael@0: michael@0: private ANRReporter() { michael@0: } michael@0: michael@0: // Return the "traces.txt" file, or null if there is no such file michael@0: private static File getTracesFile() { michael@0: try { michael@0: // getprop [prop-name [default-value]] michael@0: Process propProc = (new ProcessBuilder()) michael@0: .command("/system/bin/getprop", "dalvik.vm.stack-trace-file") michael@0: .redirectErrorStream(true) michael@0: .start(); michael@0: try { michael@0: BufferedReader buf = new BufferedReader( michael@0: new InputStreamReader(propProc.getInputStream()), TRACES_LINE_SIZE); michael@0: String propVal = buf.readLine(); michael@0: if (DEBUG) { michael@0: Log.d(LOGTAG, "getprop returned " + String.valueOf(propVal)); michael@0: } michael@0: // getprop can return empty string when the prop value is empty michael@0: // or prop is undefined, treat both cases the same way michael@0: if (propVal != null && propVal.length() != 0) { michael@0: File tracesFile = new File(propVal); michael@0: if (tracesFile.isFile() && tracesFile.canRead()) { michael@0: return tracesFile; michael@0: } else if (DEBUG) { michael@0: Log.d(LOGTAG, "cannot access traces file"); michael@0: } michael@0: } else if (DEBUG) { michael@0: Log.d(LOGTAG, "empty getprop result"); michael@0: } michael@0: } finally { michael@0: propProc.destroy(); michael@0: } michael@0: } catch (IOException e) { michael@0: Log.w(LOGTAG, e); michael@0: } catch (ClassCastException e) { michael@0: Log.w(LOGTAG, e); // Bug 975436 michael@0: } michael@0: // Check most common location one last time just in case michael@0: File tracesFile = new File("/data/anr/traces.txt"); michael@0: if (tracesFile.isFile() && tracesFile.canRead()) { michael@0: return tracesFile; michael@0: } michael@0: return null; michael@0: } michael@0: michael@0: private static File getPingFile() { michael@0: if (GeckoAppShell.getContext() == null) { michael@0: return null; michael@0: } michael@0: GeckoProfile profile = GeckoAppShell.getGeckoInterface().getProfile(); michael@0: if (profile == null) { michael@0: return null; michael@0: } michael@0: File profDir = profile.getDir(); michael@0: if (profDir == null) { michael@0: return null; michael@0: } michael@0: File pingDir = new File(profDir, "saved-telemetry-pings"); michael@0: pingDir.mkdirs(); michael@0: if (!(pingDir.exists() && pingDir.isDirectory())) { michael@0: return null; michael@0: } michael@0: return new File(pingDir, UUID.randomUUID().toString()); michael@0: } michael@0: michael@0: // Return true if the traces file corresponds to a Gecko ANR michael@0: private static boolean isGeckoTraces(String pkgName, File tracesFile) { michael@0: try { michael@0: final String END_OF_PACKAGE_NAME = "([^a-zA-Z0-9_]|$)"; michael@0: // Regex for finding our package name in the traces file michael@0: Pattern pkgPattern = Pattern.compile(Pattern.quote(pkgName) + END_OF_PACKAGE_NAME); michael@0: Pattern mangledPattern = null; michael@0: if (!AppConstants.MANGLED_ANDROID_PACKAGE_NAME.equals(pkgName)) { michael@0: mangledPattern = Pattern.compile(Pattern.quote( michael@0: AppConstants.MANGLED_ANDROID_PACKAGE_NAME) + END_OF_PACKAGE_NAME); michael@0: } michael@0: if (DEBUG) { michael@0: Log.d(LOGTAG, "trying to match package: " + pkgName); michael@0: } michael@0: BufferedReader traces = new BufferedReader( michael@0: new FileReader(tracesFile), TRACES_BLOCK_SIZE); michael@0: try { michael@0: for (int count = 0; count < LINES_TO_IDENTIFY_TRACES; count++) { michael@0: String line = traces.readLine(); michael@0: if (DEBUG) { michael@0: Log.d(LOGTAG, "identifying line: " + String.valueOf(line)); michael@0: } michael@0: if (line == null) { michael@0: if (DEBUG) { michael@0: Log.d(LOGTAG, "reached end of traces file"); michael@0: } michael@0: return false; michael@0: } michael@0: if (pkgPattern.matcher(line).find()) { michael@0: // traces.txt file contains our package michael@0: return true; michael@0: } michael@0: if (mangledPattern != null && mangledPattern.matcher(line).find()) { michael@0: // traces.txt file contains our alternate package michael@0: return true; michael@0: } michael@0: } michael@0: } finally { michael@0: traces.close(); michael@0: } michael@0: } catch (IOException e) { michael@0: // meh, can't even read from it right. just return false michael@0: } michael@0: return false; michael@0: } michael@0: michael@0: private static long getUptimeMins() { michael@0: michael@0: long uptimeMins = (new File("/proc/self/stat")).lastModified(); michael@0: if (uptimeMins != 0L) { michael@0: uptimeMins = (System.currentTimeMillis() - uptimeMins) / 1000L / 60L; michael@0: if (DEBUG) { michael@0: Log.d(LOGTAG, "uptime " + String.valueOf(uptimeMins)); michael@0: } michael@0: return uptimeMins; michael@0: } else if (DEBUG) { michael@0: Log.d(LOGTAG, "could not get uptime"); michael@0: } michael@0: return 0L; michael@0: } michael@0: michael@0: /* michael@0: a saved telemetry ping file consists of JSON in the following format, michael@0: { michael@0: "reason": "android-anr-report", michael@0: "slug": "", michael@0: "payload": michael@0: } michael@0: for Android ANR, our JSON payload should look like, michael@0: { michael@0: "ver": 1, michael@0: "simpleMeasurements": { michael@0: "uptime": michael@0: }, michael@0: "info": { michael@0: "reason": "android-anr-report", michael@0: "OS": "Android", michael@0: ... michael@0: }, michael@0: "androidANR": "...", michael@0: "androidLogcat": "..." michael@0: } michael@0: */ michael@0: michael@0: private static int writePingPayload(OutputStream ping, michael@0: String payload) throws IOException { michael@0: byte [] data = payload.getBytes(PING_CHARSET); michael@0: ping.write(data); michael@0: return data.length; michael@0: } michael@0: michael@0: private static void fillPingHeader(OutputStream ping, String slug) michael@0: throws IOException { michael@0: michael@0: // ping file header michael@0: byte [] data = ("{" + michael@0: "\"reason\":\"android-anr-report\"," + michael@0: "\"slug\":" + JSONObject.quote(slug) + "," + michael@0: "\"payload\":").getBytes(PING_CHARSET); michael@0: ping.write(data); michael@0: if (DEBUG) { michael@0: Log.d(LOGTAG, "wrote ping header, size = " + String.valueOf(data.length)); michael@0: } michael@0: michael@0: // payload start michael@0: int size = writePingPayload(ping, ("{" + michael@0: "\"ver\":1," + michael@0: "\"simpleMeasurements\":{" + michael@0: "\"uptime\":" + String.valueOf(getUptimeMins()) + michael@0: "}," + michael@0: "\"info\":{" + michael@0: "\"reason\":\"android-anr-report\"," + michael@0: "\"OS\":" + JSONObject.quote(SysInfo.getName()) + "," + michael@0: "\"version\":\"" + String.valueOf(SysInfo.getVersion()) + "\"," + michael@0: "\"appID\":" + JSONObject.quote(AppConstants.MOZ_APP_ID) + "," + michael@0: "\"appVersion\":" + JSONObject.quote(AppConstants.MOZ_APP_VERSION)+ "," + michael@0: "\"appName\":" + JSONObject.quote(AppConstants.MOZ_APP_BASENAME) + "," + michael@0: "\"appBuildID\":" + JSONObject.quote(AppConstants.MOZ_APP_BUILDID) + "," + michael@0: "\"appUpdateChannel\":" + JSONObject.quote(AppConstants.MOZ_UPDATE_CHANNEL) + "," + michael@0: // Technically the platform build ID may be different, but we'll never know michael@0: "\"platformBuildID\":" + JSONObject.quote(AppConstants.MOZ_APP_BUILDID) + "," + michael@0: "\"locale\":" + JSONObject.quote(SysInfo.getLocale()) + "," + michael@0: "\"cpucount\":" + String.valueOf(SysInfo.getCPUCount()) + "," + michael@0: "\"memsize\":" + String.valueOf(SysInfo.getMemSize()) + "," + michael@0: "\"arch\":" + JSONObject.quote(SysInfo.getArchABI()) + "," + michael@0: "\"kernel_version\":" + JSONObject.quote(SysInfo.getKernelVersion()) + "," + michael@0: "\"device\":" + JSONObject.quote(SysInfo.getDevice()) + "," + michael@0: "\"manufacturer\":" + JSONObject.quote(SysInfo.getManufacturer()) + "," + michael@0: "\"hardware\":" + JSONObject.quote(SysInfo.getHardware()) + michael@0: "}," + michael@0: "\"androidANR\":\"")); michael@0: if (DEBUG) { michael@0: Log.d(LOGTAG, "wrote metadata, size = " + String.valueOf(size)); michael@0: } michael@0: michael@0: // We are at the start of ANR data michael@0: } michael@0: michael@0: // Block is a section of the larger input stream, and we want to find pattern within michael@0: // the stream. This is straightforward if the entire pattern is within one block; michael@0: // however, if the pattern spans across two blocks, we have to match both the start of michael@0: // the pattern in the first block and the end of the pattern in the second block. michael@0: // * If pattern is found in block, this method returns the index at the end of the michael@0: // found pattern, which must always be > 0. michael@0: // * If pattern is not found, it returns 0. michael@0: // * If the start of the pattern matches the end of the block, it returns a number michael@0: // < 0, which equals the negated value of how many characters in pattern are already michael@0: // matched; when processing the next block, this number is passed in through michael@0: // prevIndex, and the rest of the characters in pattern are matched against the michael@0: // start of this second block. The method returns value > 0 if the rest of the michael@0: // characters match, or 0 if they do not. michael@0: private static int getEndPatternIndex(String block, String pattern, int prevIndex) { michael@0: if (pattern == null || block.length() < pattern.length()) { michael@0: // Nothing to do michael@0: return 0; michael@0: } michael@0: if (prevIndex < 0) { michael@0: // Last block ended with a partial start; now match start of block to rest of pattern michael@0: if (block.startsWith(pattern.substring(-prevIndex, pattern.length()))) { michael@0: // Rest of pattern matches; return index at end of pattern michael@0: return pattern.length() + prevIndex; michael@0: } michael@0: // Not a match; continue with normal search michael@0: } michael@0: // Did not find pattern in last block; see if entire pattern is inside this block michael@0: int index = block.indexOf(pattern); michael@0: if (index >= 0) { michael@0: // Found pattern; return index at end of the pattern michael@0: return index + pattern.length(); michael@0: } michael@0: // Block does not contain the entire pattern, but see if the end of the block michael@0: // contains the start of pattern. To do that, we see if block ends with the michael@0: // first n-1 characters of pattern, the first n-2 characters of pattern, etc. michael@0: for (index = block.length() - pattern.length() + 1; index < block.length(); index++) { michael@0: // Using index as a start, see if the rest of block contains the start of pattern michael@0: if (block.charAt(index) == pattern.charAt(0) && michael@0: block.endsWith(pattern.substring(0, block.length() - index))) { michael@0: // Found partial match; return -(number of characters matched), michael@0: // i.e. -1 for 1 character matched, -2 for 2 characters matched, etc. michael@0: return index - block.length(); michael@0: } michael@0: } michael@0: return 0; michael@0: } michael@0: michael@0: // Copy the content of reader to ping; michael@0: // copying stops when endPattern is found in the input stream michael@0: private static int fillPingBlock(OutputStream ping, michael@0: Reader reader, String endPattern) michael@0: throws IOException { michael@0: michael@0: int total = 0; michael@0: int endIndex = 0; michael@0: char [] block = new char[TRACES_BLOCK_SIZE]; michael@0: for (int size = reader.read(block); size >= 0; size = reader.read(block)) { michael@0: String stringBlock = new String(block, 0, size); michael@0: endIndex = getEndPatternIndex(stringBlock, endPattern, endIndex); michael@0: if (endIndex > 0) { michael@0: // Found end pattern; clip the string michael@0: stringBlock = stringBlock.substring(0, endIndex); michael@0: } michael@0: String quoted = JSONObject.quote(stringBlock); michael@0: total += writePingPayload(ping, quoted.substring(1, quoted.length() - 1)); michael@0: if (endIndex > 0) { michael@0: // End pattern already found; return now michael@0: break; michael@0: } michael@0: } michael@0: return total; michael@0: } michael@0: michael@0: private static void fillPingFooter(OutputStream ping, michael@0: boolean haveNativeStack) michael@0: throws IOException { michael@0: michael@0: // We are at the end of ANR data michael@0: michael@0: int total = writePingPayload(ping, ("\"," + michael@0: "\"androidLogcat\":\"")); michael@0: michael@0: try { michael@0: // get the last 200 lines of logcat michael@0: Process proc = (new ProcessBuilder()) michael@0: .command("/system/bin/logcat", "-v", "threadtime", "-t", "200", "-d", "*:D") michael@0: .redirectErrorStream(true) michael@0: .start(); michael@0: try { michael@0: Reader procOut = new InputStreamReader(proc.getInputStream(), TRACES_CHARSET); michael@0: int size = fillPingBlock(ping, procOut, null); michael@0: if (DEBUG) { michael@0: Log.d(LOGTAG, "wrote logcat, size = " + String.valueOf(size)); michael@0: } michael@0: } finally { michael@0: proc.destroy(); michael@0: } michael@0: } catch (IOException e) { michael@0: // ignore because logcat is not essential michael@0: Log.w(LOGTAG, e); michael@0: } michael@0: michael@0: if (haveNativeStack) { michael@0: total += writePingPayload(ping, ("\"," + michael@0: "\"androidNativeStack\":")); michael@0: michael@0: String nativeStack = String.valueOf(getNativeStack()); michael@0: int size = writePingPayload(ping, nativeStack); michael@0: if (DEBUG) { michael@0: Log.d(LOGTAG, "wrote native stack, size = " + String.valueOf(size)); michael@0: } michael@0: total += size + writePingPayload(ping, "}"); michael@0: } else { michael@0: total += writePingPayload(ping, "\"}"); michael@0: } michael@0: michael@0: byte [] data = ( michael@0: "}").getBytes(PING_CHARSET); michael@0: ping.write(data); michael@0: if (DEBUG) { michael@0: Log.d(LOGTAG, "wrote ping footer, size = " + String.valueOf(data.length + total)); michael@0: } michael@0: } michael@0: michael@0: private static void processTraces(Reader traces, File pingFile) { michael@0: michael@0: // Unwinding is memory intensive; only unwind if we have enough memory michael@0: boolean haveNativeStack = requestNativeStack( michael@0: /* unwind */ SysInfo.getMemSize() >= 640); michael@0: try { michael@0: OutputStream ping = new BufferedOutputStream( michael@0: new FileOutputStream(pingFile), TRACES_BLOCK_SIZE); michael@0: try { michael@0: fillPingHeader(ping, pingFile.getName()); michael@0: // Traces file has the format michael@0: // ----- pid xxx at xxx ----- michael@0: // Cmd line: org.mozilla.xxx michael@0: // * stack trace * michael@0: // ----- end xxx ----- michael@0: // ----- pid xxx at xxx ----- michael@0: // Cmd line: com.android.xxx michael@0: // * stack trace * michael@0: // ... michael@0: // If we end the stack dump at the first end marker, michael@0: // only Fennec stacks will be dumped michael@0: int size = fillPingBlock(ping, traces, "\n----- end"); michael@0: if (DEBUG) { michael@0: Log.d(LOGTAG, "wrote traces, size = " + String.valueOf(size)); michael@0: } michael@0: fillPingFooter(ping, haveNativeStack); michael@0: if (DEBUG) { michael@0: Log.d(LOGTAG, "finished creating ping file"); michael@0: } michael@0: return; michael@0: } finally { michael@0: ping.close(); michael@0: if (haveNativeStack) { michael@0: releaseNativeStack(); michael@0: } michael@0: } michael@0: } catch (IOException e) { michael@0: Log.w(LOGTAG, e); michael@0: } michael@0: // exception; delete ping file michael@0: if (pingFile.exists()) { michael@0: pingFile.delete(); michael@0: } michael@0: } michael@0: michael@0: private static void processTraces(File tracesFile, File pingFile) { michael@0: try { michael@0: Reader traces = new InputStreamReader( michael@0: new FileInputStream(tracesFile), TRACES_CHARSET); michael@0: try { michael@0: processTraces(traces, pingFile); michael@0: } finally { michael@0: traces.close(); michael@0: } michael@0: } catch (IOException e) { michael@0: Log.w(LOGTAG, e); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void onReceive(Context context, Intent intent) { michael@0: if (mPendingANR) { michael@0: // we already processed an ANR without getting unstuck; skip this one michael@0: if (DEBUG) { michael@0: Log.d(LOGTAG, "skipping duplicate ANR"); michael@0: } michael@0: return; michael@0: } michael@0: if (ThreadUtils.getUiHandler() != null) { michael@0: mPendingANR = true; michael@0: // detect when the main thread gets unstuck michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: // okay to reset mPendingANR on main thread michael@0: mPendingANR = false; michael@0: if (DEBUG) { michael@0: Log.d(LOGTAG, "yay we got unstuck!"); michael@0: } michael@0: } michael@0: }); michael@0: } michael@0: if (DEBUG) { michael@0: Log.d(LOGTAG, "receiving " + String.valueOf(intent)); michael@0: } michael@0: if (!ANR_ACTION.equals(intent.getAction())) { michael@0: return; michael@0: } michael@0: michael@0: // make sure we have a good save location first michael@0: File pingFile = getPingFile(); michael@0: if (DEBUG) { michael@0: Log.d(LOGTAG, "using ping file: " + String.valueOf(pingFile)); michael@0: } michael@0: if (pingFile == null) { michael@0: return; michael@0: } michael@0: michael@0: File tracesFile = getTracesFile(); michael@0: if (DEBUG) { michael@0: Log.d(LOGTAG, "using traces file: " + String.valueOf(tracesFile)); michael@0: } michael@0: if (tracesFile == null) { michael@0: return; michael@0: } michael@0: michael@0: // We get ANR intents from all ANRs in the system, but we only want Gecko ANRs michael@0: if (!isGeckoTraces(context.getPackageName(), tracesFile)) { michael@0: if (DEBUG) { michael@0: Log.d(LOGTAG, "traces is not Gecko ANR"); michael@0: } michael@0: return; michael@0: } michael@0: Log.i(LOGTAG, "processing Gecko ANR"); michael@0: processTraces(tracesFile, pingFile); michael@0: } michael@0: }