mobile/android/base/ANRReporter.java

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
     2  * This Source Code Form is subject to the terms of the Mozilla Public
     3  * License, v. 2.0. If a copy of the MPL was not distributed with this
     4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     6 package org.mozilla.gecko;
     8 import java.io.BufferedOutputStream;
     9 import java.io.BufferedReader;
    10 import java.io.File;
    11 import java.io.FileInputStream;
    12 import java.io.FileOutputStream;
    13 import java.io.FileReader;
    14 import java.io.IOException;
    15 import java.io.InputStreamReader;
    16 import java.io.OutputStream;
    17 import java.io.Reader;
    18 import java.util.UUID;
    19 import java.util.regex.Pattern;
    21 import org.json.JSONObject;
    22 import org.mozilla.gecko.util.ThreadUtils;
    24 import android.content.BroadcastReceiver;
    25 import android.content.Context;
    26 import android.content.Intent;
    27 import android.content.IntentFilter;
    28 import android.os.Handler;
    29 import android.os.Looper;
    30 import android.util.Log;
    32 public final class ANRReporter extends BroadcastReceiver
    33 {
    34     private static final boolean DEBUG = false;
    35     private static final String LOGTAG = "GeckoANRReporter";
    37     private static final String ANR_ACTION = "android.intent.action.ANR";
    38     // Number of lines to search traces.txt to decide whether it's a Gecko ANR
    39     private static final int LINES_TO_IDENTIFY_TRACES = 10;
    40     // ANRs may happen because of memory pressure,
    41     //  so don't use up too much memory here
    42     // Size of buffer to hold one line of text
    43     private static final int TRACES_LINE_SIZE = 100;
    44     // Size of block to use when processing traces.txt
    45     private static final int TRACES_BLOCK_SIZE = 2000;
    46     private static final String TRACES_CHARSET = "utf-8";
    47     private static final String PING_CHARSET = "utf-8";
    49     private static final ANRReporter sInstance = new ANRReporter();
    50     private static int sRegisteredCount;
    51     private Handler mHandler;
    52     private volatile boolean mPendingANR;
    54     private static native boolean requestNativeStack(boolean unwind);
    55     private static native String getNativeStack();
    56     private static native void releaseNativeStack();
    58     public static void register(Context context) {
    59         if (sRegisteredCount++ != 0) {
    60             // Already registered
    61             return;
    62         }
    63         sInstance.start(context);
    64     }
    66     public static void unregister() {
    67         if (sRegisteredCount == 0) {
    68             Log.w(LOGTAG, "register/unregister mismatch");
    69             return;
    70         }
    71         if (--sRegisteredCount != 0) {
    72             // Should still be registered
    73             return;
    74         }
    75         sInstance.stop();
    76     }
    78     private void start(final Context context) {
    80         Thread receiverThread = new Thread(new Runnable() {
    81             @Override
    82             public void run() {
    83                 Looper.prepare();
    84                 synchronized (ANRReporter.this) {
    85                     mHandler = new Handler();
    86                     ANRReporter.this.notify();
    87                 }
    88                 if (DEBUG) {
    89                     Log.d(LOGTAG, "registering receiver");
    90                 }
    91                 context.registerReceiver(ANRReporter.this,
    92                                          new IntentFilter(ANR_ACTION),
    93                                          null,
    94                                          mHandler);
    95                 Looper.loop();
    97                 if (DEBUG) {
    98                     Log.d(LOGTAG, "unregistering receiver");
    99                 }
   100                 context.unregisterReceiver(ANRReporter.this);
   101                 mHandler = null;
   102             }
   103         }, LOGTAG);
   105         receiverThread.setDaemon(true);
   106         receiverThread.start();
   107     }
   109     private void stop() {
   110         synchronized (this) {
   111             while (mHandler == null) {
   112                 try {
   113                     wait(1000);
   114                     if (mHandler == null) {
   115                         // We timed out; just give up. The process is probably
   116                         // quitting anyways, so we let the OS do the clean up
   117                         Log.w(LOGTAG, "timed out waiting for handler");
   118                         return;
   119                     }
   120                 } catch (InterruptedException e) {
   121                 }
   122             }
   123         }
   124         Looper looper = mHandler.getLooper();
   125         looper.quit();
   126         try {
   127             looper.getThread().join();
   128         } catch (InterruptedException e) {
   129         }
   130     }
   132     private ANRReporter() {
   133     }
   135     // Return the "traces.txt" file, or null if there is no such file
   136     private static File getTracesFile() {
   137         try {
   138             // getprop [prop-name [default-value]]
   139             Process propProc = (new ProcessBuilder())
   140                 .command("/system/bin/getprop", "dalvik.vm.stack-trace-file")
   141                 .redirectErrorStream(true)
   142                 .start();
   143             try {
   144                 BufferedReader buf = new BufferedReader(
   145                     new InputStreamReader(propProc.getInputStream()), TRACES_LINE_SIZE);
   146                 String propVal = buf.readLine();
   147                 if (DEBUG) {
   148                     Log.d(LOGTAG, "getprop returned " + String.valueOf(propVal));
   149                 }
   150                 // getprop can return empty string when the prop value is empty
   151                 // or prop is undefined, treat both cases the same way
   152                 if (propVal != null && propVal.length() != 0) {
   153                     File tracesFile = new File(propVal);
   154                     if (tracesFile.isFile() && tracesFile.canRead()) {
   155                         return tracesFile;
   156                     } else if (DEBUG) {
   157                         Log.d(LOGTAG, "cannot access traces file");
   158                     }
   159                 } else if (DEBUG) {
   160                     Log.d(LOGTAG, "empty getprop result");
   161                 }
   162             } finally {
   163                 propProc.destroy();
   164             }
   165         } catch (IOException e) {
   166             Log.w(LOGTAG, e);
   167         } catch (ClassCastException e) {
   168             Log.w(LOGTAG, e); // Bug 975436
   169         }
   170         // Check most common location one last time just in case
   171         File tracesFile = new File("/data/anr/traces.txt");
   172         if (tracesFile.isFile() && tracesFile.canRead()) {
   173             return tracesFile;
   174         }
   175         return null;
   176     }
   178     private static File getPingFile() {
   179         if (GeckoAppShell.getContext() == null) {
   180             return null;
   181         }
   182         GeckoProfile profile = GeckoAppShell.getGeckoInterface().getProfile();
   183         if (profile == null) {
   184             return null;
   185         }
   186         File profDir = profile.getDir();
   187         if (profDir == null) {
   188             return null;
   189         }
   190         File pingDir = new File(profDir, "saved-telemetry-pings");
   191         pingDir.mkdirs();
   192         if (!(pingDir.exists() && pingDir.isDirectory())) {
   193             return null;
   194         }
   195         return new File(pingDir, UUID.randomUUID().toString());
   196     }
   198     // Return true if the traces file corresponds to a Gecko ANR
   199     private static boolean isGeckoTraces(String pkgName, File tracesFile) {
   200         try {
   201             final String END_OF_PACKAGE_NAME = "([^a-zA-Z0-9_]|$)";
   202             // Regex for finding our package name in the traces file
   203             Pattern pkgPattern = Pattern.compile(Pattern.quote(pkgName) + END_OF_PACKAGE_NAME);
   204             Pattern mangledPattern = null;
   205             if (!AppConstants.MANGLED_ANDROID_PACKAGE_NAME.equals(pkgName)) {
   206                 mangledPattern = Pattern.compile(Pattern.quote(
   207                     AppConstants.MANGLED_ANDROID_PACKAGE_NAME) + END_OF_PACKAGE_NAME);
   208             }
   209             if (DEBUG) {
   210                 Log.d(LOGTAG, "trying to match package: " + pkgName);
   211             }
   212             BufferedReader traces = new BufferedReader(
   213                 new FileReader(tracesFile), TRACES_BLOCK_SIZE);
   214             try {
   215                 for (int count = 0; count < LINES_TO_IDENTIFY_TRACES; count++) {
   216                     String line = traces.readLine();
   217                     if (DEBUG) {
   218                         Log.d(LOGTAG, "identifying line: " + String.valueOf(line));
   219                     }
   220                     if (line == null) {
   221                         if (DEBUG) {
   222                             Log.d(LOGTAG, "reached end of traces file");
   223                         }
   224                         return false;
   225                     }
   226                     if (pkgPattern.matcher(line).find()) {
   227                         // traces.txt file contains our package
   228                         return true;
   229                     }
   230                     if (mangledPattern != null && mangledPattern.matcher(line).find()) {
   231                         // traces.txt file contains our alternate package
   232                         return true;
   233                     }
   234                 }
   235             } finally {
   236                 traces.close();
   237             }
   238         } catch (IOException e) {
   239             // meh, can't even read from it right. just return false
   240         }
   241         return false;
   242     }
   244     private static long getUptimeMins() {
   246         long uptimeMins = (new File("/proc/self/stat")).lastModified();
   247         if (uptimeMins != 0L) {
   248             uptimeMins = (System.currentTimeMillis() - uptimeMins) / 1000L / 60L;
   249             if (DEBUG) {
   250                 Log.d(LOGTAG, "uptime " + String.valueOf(uptimeMins));
   251             }
   252             return uptimeMins;
   253         } else if (DEBUG) {
   254             Log.d(LOGTAG, "could not get uptime");
   255         }
   256         return 0L;
   257     }
   259     /*
   260         a saved telemetry ping file consists of JSON in the following format,
   261             {
   262                 "reason": "android-anr-report",
   263                 "slug": "<uuid-string>",
   264                 "payload": <json-object>
   265             }
   266         for Android ANR, our JSON payload should look like,
   267             {
   268                 "ver": 1,
   269                 "simpleMeasurements": {
   270                     "uptime": <uptime>
   271                 },
   272                 "info": {
   273                     "reason": "android-anr-report",
   274                     "OS": "Android",
   275                     ...
   276                 },
   277                 "androidANR": "...",
   278                 "androidLogcat": "..."
   279             }
   280     */
   282     private static int writePingPayload(OutputStream ping,
   283                                         String payload) throws IOException {
   284         byte [] data = payload.getBytes(PING_CHARSET);
   285         ping.write(data);
   286         return data.length;
   287     }
   289     private static void fillPingHeader(OutputStream ping, String slug)
   290             throws IOException {
   292         // ping file header
   293         byte [] data = ("{" +
   294             "\"reason\":\"android-anr-report\"," +
   295             "\"slug\":" + JSONObject.quote(slug) + "," +
   296             "\"payload\":").getBytes(PING_CHARSET);
   297         ping.write(data);
   298         if (DEBUG) {
   299             Log.d(LOGTAG, "wrote ping header, size = " + String.valueOf(data.length));
   300         }
   302         // payload start
   303         int size = writePingPayload(ping, ("{" +
   304             "\"ver\":1," +
   305             "\"simpleMeasurements\":{" +
   306                 "\"uptime\":" + String.valueOf(getUptimeMins()) +
   307             "}," +
   308             "\"info\":{" +
   309                 "\"reason\":\"android-anr-report\"," +
   310                 "\"OS\":" + JSONObject.quote(SysInfo.getName()) + "," +
   311                 "\"version\":\"" + String.valueOf(SysInfo.getVersion()) + "\"," +
   312                 "\"appID\":" + JSONObject.quote(AppConstants.MOZ_APP_ID) + "," +
   313                 "\"appVersion\":" + JSONObject.quote(AppConstants.MOZ_APP_VERSION)+ "," +
   314                 "\"appName\":" + JSONObject.quote(AppConstants.MOZ_APP_BASENAME) + "," +
   315                 "\"appBuildID\":" + JSONObject.quote(AppConstants.MOZ_APP_BUILDID) + "," +
   316                 "\"appUpdateChannel\":" + JSONObject.quote(AppConstants.MOZ_UPDATE_CHANNEL) + "," +
   317                 // Technically the platform build ID may be different, but we'll never know
   318                 "\"platformBuildID\":" + JSONObject.quote(AppConstants.MOZ_APP_BUILDID) + "," +
   319                 "\"locale\":" + JSONObject.quote(SysInfo.getLocale()) + "," +
   320                 "\"cpucount\":" + String.valueOf(SysInfo.getCPUCount()) + "," +
   321                 "\"memsize\":" + String.valueOf(SysInfo.getMemSize()) + "," +
   322                 "\"arch\":" + JSONObject.quote(SysInfo.getArchABI()) + "," +
   323                 "\"kernel_version\":" + JSONObject.quote(SysInfo.getKernelVersion()) + "," +
   324                 "\"device\":" + JSONObject.quote(SysInfo.getDevice()) + "," +
   325                 "\"manufacturer\":" + JSONObject.quote(SysInfo.getManufacturer()) + "," +
   326                 "\"hardware\":" + JSONObject.quote(SysInfo.getHardware()) +
   327             "}," +
   328             "\"androidANR\":\""));
   329         if (DEBUG) {
   330             Log.d(LOGTAG, "wrote metadata, size = " + String.valueOf(size));
   331         }
   333         // We are at the start of ANR data
   334     }
   336     // Block is a section of the larger input stream, and we want to find pattern within
   337     // the stream. This is straightforward if the entire pattern is within one block;
   338     // however, if the pattern spans across two blocks, we have to match both the start of
   339     // the pattern in the first block and the end of the pattern in the second block.
   340     // * If pattern is found in block, this method returns the index at the end of the
   341     //   found pattern, which must always be > 0.
   342     // * If pattern is not found, it returns 0.
   343     // * If the start of the pattern matches the end of the block, it returns a number
   344     //   < 0, which equals the negated value of how many characters in pattern are already
   345     //   matched; when processing the next block, this number is passed in through
   346     //   prevIndex, and the rest of the characters in pattern are matched against the
   347     //   start of this second block. The method returns value > 0 if the rest of the
   348     //   characters match, or 0 if they do not.
   349     private static int getEndPatternIndex(String block, String pattern, int prevIndex) {
   350         if (pattern == null || block.length() < pattern.length()) {
   351             // Nothing to do
   352             return 0;
   353         }
   354         if (prevIndex < 0) {
   355             // Last block ended with a partial start; now match start of block to rest of pattern
   356             if (block.startsWith(pattern.substring(-prevIndex, pattern.length()))) {
   357                 // Rest of pattern matches; return index at end of pattern
   358                 return pattern.length() + prevIndex;
   359             }
   360             // Not a match; continue with normal search
   361         }
   362         // Did not find pattern in last block; see if entire pattern is inside this block
   363         int index = block.indexOf(pattern);
   364         if (index >= 0) {
   365             // Found pattern; return index at end of the pattern
   366             return index + pattern.length();
   367         }
   368         // Block does not contain the entire pattern, but see if the end of the block
   369         // contains the start of pattern. To do that, we see if block ends with the
   370         // first n-1 characters of pattern, the first n-2 characters of pattern, etc.
   371         for (index = block.length() - pattern.length() + 1; index < block.length(); index++) {
   372             // Using index as a start, see if the rest of block contains the start of pattern
   373             if (block.charAt(index) == pattern.charAt(0) &&
   374                 block.endsWith(pattern.substring(0, block.length() - index))) {
   375                 // Found partial match; return -(number of characters matched),
   376                 // i.e. -1 for 1 character matched, -2 for 2 characters matched, etc.
   377                 return index - block.length();
   378             }
   379         }
   380         return 0;
   381     }
   383     // Copy the content of reader to ping;
   384     // copying stops when endPattern is found in the input stream
   385     private static int fillPingBlock(OutputStream ping,
   386                                      Reader reader, String endPattern)
   387             throws IOException {
   389         int total = 0;
   390         int endIndex = 0;
   391         char [] block = new char[TRACES_BLOCK_SIZE];
   392         for (int size = reader.read(block); size >= 0; size = reader.read(block)) {
   393             String stringBlock = new String(block, 0, size);
   394             endIndex = getEndPatternIndex(stringBlock, endPattern, endIndex);
   395             if (endIndex > 0) {
   396                 // Found end pattern; clip the string
   397                 stringBlock = stringBlock.substring(0, endIndex);
   398             }
   399             String quoted = JSONObject.quote(stringBlock);
   400             total += writePingPayload(ping, quoted.substring(1, quoted.length() - 1));
   401             if (endIndex > 0) {
   402                 // End pattern already found; return now
   403                 break;
   404             }
   405         }
   406         return total;
   407     }
   409     private static void fillPingFooter(OutputStream ping,
   410                                        boolean haveNativeStack)
   411             throws IOException {
   413         // We are at the end of ANR data
   415         int total = writePingPayload(ping, ("\"," +
   416                 "\"androidLogcat\":\""));
   418         try {
   419             // get the last 200 lines of logcat
   420             Process proc = (new ProcessBuilder())
   421                 .command("/system/bin/logcat", "-v", "threadtime", "-t", "200", "-d", "*:D")
   422                 .redirectErrorStream(true)
   423                 .start();
   424             try {
   425                 Reader procOut = new InputStreamReader(proc.getInputStream(), TRACES_CHARSET);
   426                 int size = fillPingBlock(ping, procOut, null);
   427                 if (DEBUG) {
   428                     Log.d(LOGTAG, "wrote logcat, size = " + String.valueOf(size));
   429                 }
   430             } finally {
   431                 proc.destroy();
   432             }
   433         } catch (IOException e) {
   434             // ignore because logcat is not essential
   435             Log.w(LOGTAG, e);
   436         }
   438         if (haveNativeStack) {
   439             total += writePingPayload(ping, ("\"," +
   440                     "\"androidNativeStack\":"));
   442             String nativeStack = String.valueOf(getNativeStack());
   443             int size = writePingPayload(ping, nativeStack);
   444             if (DEBUG) {
   445                 Log.d(LOGTAG, "wrote native stack, size = " + String.valueOf(size));
   446             }
   447             total += size + writePingPayload(ping, "}");
   448         } else {
   449             total += writePingPayload(ping, "\"}");
   450         }
   452         byte [] data = (
   453             "}").getBytes(PING_CHARSET);
   454         ping.write(data);
   455         if (DEBUG) {
   456             Log.d(LOGTAG, "wrote ping footer, size = " + String.valueOf(data.length + total));
   457         }
   458     }
   460     private static void processTraces(Reader traces, File pingFile) {
   462         // Unwinding is memory intensive; only unwind if we have enough memory
   463         boolean haveNativeStack = requestNativeStack(
   464             /* unwind */ SysInfo.getMemSize() >= 640);
   465         try {
   466             OutputStream ping = new BufferedOutputStream(
   467                 new FileOutputStream(pingFile), TRACES_BLOCK_SIZE);
   468             try {
   469                 fillPingHeader(ping, pingFile.getName());
   470                 // Traces file has the format
   471                 //    ----- pid xxx at xxx -----
   472                 //    Cmd line: org.mozilla.xxx
   473                 //    * stack trace *
   474                 //    ----- end xxx -----
   475                 //    ----- pid xxx at xxx -----
   476                 //    Cmd line: com.android.xxx
   477                 //    * stack trace *
   478                 //    ...
   479                 // If we end the stack dump at the first end marker,
   480                 // only Fennec stacks will be dumped
   481                 int size = fillPingBlock(ping, traces, "\n----- end");
   482                 if (DEBUG) {
   483                     Log.d(LOGTAG, "wrote traces, size = " + String.valueOf(size));
   484                 }
   485                 fillPingFooter(ping, haveNativeStack);
   486                 if (DEBUG) {
   487                     Log.d(LOGTAG, "finished creating ping file");
   488                 }
   489                 return;
   490             } finally {
   491                 ping.close();
   492                 if (haveNativeStack) {
   493                     releaseNativeStack();
   494                 }
   495             }
   496         } catch (IOException e) {
   497             Log.w(LOGTAG, e);
   498         }
   499         // exception; delete ping file
   500         if (pingFile.exists()) {
   501             pingFile.delete();
   502         }
   503     }
   505     private static void processTraces(File tracesFile, File pingFile) {
   506         try {
   507             Reader traces = new InputStreamReader(
   508                 new FileInputStream(tracesFile), TRACES_CHARSET);
   509             try {
   510                 processTraces(traces, pingFile);
   511             } finally {
   512                 traces.close();
   513             }
   514         } catch (IOException e) {
   515             Log.w(LOGTAG, e);
   516         }
   517     }
   519     @Override
   520     public void onReceive(Context context, Intent intent) {
   521         if (mPendingANR) {
   522             // we already processed an ANR without getting unstuck; skip this one
   523             if (DEBUG) {
   524                 Log.d(LOGTAG, "skipping duplicate ANR");
   525             }
   526             return;
   527         }
   528         if (ThreadUtils.getUiHandler() != null) {
   529             mPendingANR = true;
   530             // detect when the main thread gets unstuck
   531             ThreadUtils.postToUiThread(new Runnable() {
   532                 @Override
   533                 public void run() {
   534                     // okay to reset mPendingANR on main thread
   535                     mPendingANR = false;
   536                     if (DEBUG) {
   537                         Log.d(LOGTAG, "yay we got unstuck!");
   538                     }
   539                 }
   540             });
   541         }
   542         if (DEBUG) {
   543             Log.d(LOGTAG, "receiving " + String.valueOf(intent));
   544         }
   545         if (!ANR_ACTION.equals(intent.getAction())) {
   546             return;
   547         }
   549         // make sure we have a good save location first
   550         File pingFile = getPingFile();
   551         if (DEBUG) {
   552             Log.d(LOGTAG, "using ping file: " + String.valueOf(pingFile));
   553         }
   554         if (pingFile == null) {
   555             return;
   556         }
   558         File tracesFile = getTracesFile();
   559         if (DEBUG) {
   560             Log.d(LOGTAG, "using traces file: " + String.valueOf(tracesFile));
   561         }
   562         if (tracesFile == null) {
   563             return;
   564         }
   566         // We get ANR intents from all ANRs in the system, but we only want Gecko ANRs
   567         if (!isGeckoTraces(context.getPackageName(), tracesFile)) {
   568             if (DEBUG) {
   569                 Log.d(LOGTAG, "traces is not Gecko ANR");
   570             }
   571             return;
   572         }
   573         Log.i(LOGTAG, "processing Gecko ANR");
   574         processTraces(tracesFile, pingFile);
   575     }
   576 }

mercurial