Wed, 31 Dec 2014 06:09:35 +0100
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 }