/* -*- 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/. */packageorg.mozilla.gecko;importjava.io.BufferedWriter;importjava.io.File;importjava.io.FileOutputStream;importjava.io.FileReader;importjava.io.FileWriter;importjava.io.IOException;importjava.io.PrintWriter;importjava.io.StringWriter;importjava.util.Arrays;importjava.util.UUID;importandroid.content.ComponentName;importandroid.content.Context;importandroid.content.Intent;importandroid.content.pm.PackageInfo;importandroid.content.pm.PackageManager;importandroid.net.Uri;importandroid.os.Build;importandroid.os.Bundle;importandroid.os.Process;importandroid.util.Log;publicclassCrashHandlerimplementsThread.UncaughtExceptionHandler{privatestaticfinalStringLOGTAG="GeckoCrashHandler";privatestaticfinalThreadMAIN_THREAD=Thread.currentThread();privatestaticfinalStringDEFAULT_SERVER_URL="https://crash-reports.mozilla.com/submit?id=%1$s&version=%2$s&buildid=%3$s";// Context for getting device informationprotectedfinalContextappContext;// Thread that this handler applies to, or null for a global handlerprotectedfinalThreadhandlerThread;protectedfinalThread.UncaughtExceptionHandlersystemUncaughtHandler;protectedbooleancrashing;protectedbooleanunregistered;/** * Get the root exception from the 'cause' chain of an exception. * * @param exc An exception * @return The root exception */publicstaticThrowablegetRootException(Throwableexc){for(Throwablecause=exc;cause!=null;cause=cause.getCause()){exc=cause;}returnexc;}/** * Get the standard stack trace string of an exception. * * @param exc An exception * @return The exception stack trace. */publicstaticStringgetExceptionStackTrace(finalThrowableexc){StringWritersw=newStringWriter();PrintWriterpw=newPrintWriter(sw);exc.printStackTrace(pw);pw.flush();returnsw.toString();}/** * Terminate the current process. */publicstaticvoidterminateProcess(){Process.killProcess(Process.myPid());}/** * Create and register a CrashHandler for all threads and thread groups. */publicCrashHandler(){this((Context)null);}/** * Create and register a CrashHandler for all threads and thread groups. * * @param appContext A Context for retrieving application information. */publicCrashHandler(finalContextappContext){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 */publicCrashHandler(finalThreadthread){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. */publicCrashHandler(finalThreadthread,finalContextappContext){this.appContext=appContext;this.handlerThread=thread;this.systemUncaughtHandler=thread.getUncaughtExceptionHandler();thread.setUncaughtExceptionHandler(this);}/** * Unregister this CrashHandler for exception handling. */publicvoidunregister(){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 */publicstaticvoidlogException(finalThreadthread,finalThrowableexc){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(StackTraceElementste:MAIN_THREAD.getStackTrace()){Log.e(LOGTAG," "+ste.toString());}}}catch(finalThrowablee){// If something throws here, we want to continue to report the exception,// so we catch all exceptions and ignore them.}}privatestaticlonggetCrashTime(){returnSystem.currentTimeMillis()/1000;}privatestaticlonggetStartupTime(){// Process start time is also the proc file modified time.finallonguptimeMins=(newFile("/proc/self/cmdline")).lastModified();if(uptimeMins==0L){returngetCrashTime();}returnuptimeMins/1000;}privatestaticStringgetJavaPackageName(){returnCrashHandler.class.getPackage().getName();}protectedStringgetAppPackageName(){finalContextcontext=getAppContext();if(context!=null){returncontext.getPackageName();}try{// Package name is also the command line string in most cases.finalFileReaderreader=newFileReader("/proc/self/cmdline");finalchar[]buffer=newchar[64];try{if(reader.read(buffer)>0){// cmdline is delimited by '\0', and we want the first token.finalintnul=Arrays.asList(buffer).indexOf('\0');return(newString(buffer,0,nul<0?buffer.length:nul)).trim();}}finally{reader.close();}}catch(finalIOExceptione){Log.i(LOGTAG,"Error reading package name",e);}// Fallback to using CrashHandler's package name.returngetJavaPackageName();}protectedContextgetAppContext(){returnappContext;}/** * Get the crash "extras" to be reported. * * @param thread The exception thread * @param exc An exception * @return "Extras" in the from of a Bundle */protectedBundlegetCrashExtras(finalThreadthread,finalThrowableexc){finalContextcontext=getAppContext();finalBundleextras=newBundle();finalStringpkgName=getAppPackageName();extras.putString("ProductName",pkgName);extras.putLong("CrashTime",getCrashTime());extras.putLong("StartupTime",getStartupTime());if(context!=null){finalPackageManagerpkgMgr=context.getPackageManager();try{finalPackageInfopkgInfo=pkgMgr.getPackageInfo(pkgName,0);extras.putString("Version",pkgInfo.versionName);extras.putInt("BuildID",pkgInfo.versionCode);extras.putLong("InstallTime",pkgInfo.lastUpdateTime/1000);}catch(finalPackageManager.NameNotFoundExceptione){Log.i(LOGTAG,"Error getting package info",e);}}extras.putString("JavaStackTrace",getExceptionStackTrace(exc));returnextras;}/** * Get the crash minidump content to be reported. * * @param thread The exception thread * @param exc An exception * @return Minidump content */protectedbyte[]getCrashDump(finalThreadthread,finalThrowableexc){returnnewbyte[0];// No minidump.}protectedstaticStringnormalizeUrlString(finalStringstr){if(str==null){return"";}returnUri.encode(str);}/** * Get the server URL to send the crash report to. * * @param extras The crash extras Bundle */protectedStringgetServerUrl(finalBundleextras){returnString.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 */protectedbooleanlaunchCrashReporter(finalStringdumpFile,finalStringextraFile){try{finalContextcontext=getAppContext();finalStringjavaPkg=getJavaPackageName();finalStringpkg=getAppPackageName();finalStringcomponent=javaPkg+".CrashReporter";finalStringaction=javaPkg+".reportCrash";finalProcessBuilderpb;if(context!=null){finalIntentintent=newIntent(action);intent.setComponent(newComponentName(pkg,component));intent.putExtra("minidumpPath",dumpFile);intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);context.startActivity(intent);returntrue;}// Avoid AppConstants dependency for SDK version constants,// because CrashHandler could be used outside of Fennec code.if(Build.VERSION.SDK_INT<17){pb=newProcessBuilder("/system/bin/am","start","-a",action,"-n",pkg+'/'+component,"--es","minidumpPath",dumpFile);}else{pb=newProcessBuilder("/system/bin/am","start","--user",/* USER_CURRENT_OR_SELF */"-3","-a",action,"-n",pkg+'/'+component,"--es","minidumpPath",dumpFile);}pb.start().waitFor();}catch(finalIOExceptione){Log.e(LOGTAG,"Error launching crash reporter",e);returnfalse;}catch(finalInterruptedExceptione){Log.i(LOGTAG,"Interrupted while waiting to launch crash reporter",e);// Fall-through}returntrue;}/** * Report an exception to Socorro. * * @param thread The exception thread * @param exc An exception * @return Whether the exception was successfully reported */protectedbooleanreportException(finalThreadthread,finalThrowableexc){finalContextcontext=getAppContext();finalStringid=UUID.randomUUID().toString();// Use the cache directory under the app directory to store crash files.finalFiledir;if(context!=null){dir=context.getCacheDir();}else{dir=newFile("/data/data/"+getAppPackageName()+"/cache");}dir.mkdirs();if(!dir.exists()){returnfalse;}finalFiledmpFile=newFile(dir,id+".dmp");finalFileextraFile=newFile(dir,id+".extra");try{// Write out minidump file as binary.finalbyte[]minidump=getCrashDump(thread,exc);finalFileOutputStreamdmpStream=newFileOutputStream(dmpFile);try{dmpStream.write(minidump);}finally{dmpStream.close();}}catch(finalIOExceptione){Log.e(LOGTAG,"Error writing minidump file",e);returnfalse;}try{// Write out crash extra file as text.finalBundleextras=getCrashExtras(thread,exc);finalStringurl=getServerUrl(extras);extras.putString("ServerURL",url);finalBufferedWriterextraWriter=newBufferedWriter(newFileWriter(extraFile));try{for(Stringkey: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(finalIOExceptione){Log.e(LOGTAG,"Error writing extra file",e);returnfalse;}returnlaunchCrashReporter(dmpFile.getAbsolutePath(),extraFile.getAbsolutePath());}/** * Implements the default behavior for handling uncaught exceptions. * * @param thread The exception thread * @param exc An uncaught exception */@OverridepublicvoiduncaughtException(Threadthread,Throwableexc){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();}}publicstaticCrashHandlercreateDefaultCrashHandler(finalContextcontext){returnnewCrashHandler(context){@OverrideprotectedBundlegetCrashExtras(finalThreadthread,finalThrowableexc){finalBundleextras=super.getCrashExtras(thread,exc);extras.putString("ProductName",AppConstants.MOZ_APP_BASENAME);extras.putString("ProductID",AppConstants.MOZ_APP_ID);extras.putString("Version",AppConstants.MOZ_APP_VERSION);extras.putString("BuildID",AppConstants.MOZ_APP_BUILDID);extras.putString("Vendor",AppConstants.MOZ_APP_VENDOR);extras.putString("ReleaseChannel",AppConstants.MOZ_UPDATE_CHANNEL);returnextras;}@OverridepublicbooleanreportException(finalThreadthread,finalThrowableexc){if(AppConstants.MOZ_CRASHREPORTER&&AppConstants.MOZILLA_OFFICIAL){// Only use Java crash reporter if enabled on official build.returnsuper.reportException(thread,exc);}returnfalse;}};}}