2015-09-26 22:11:53 +00:00
/ *
* Apple // emulator for *nix
*
* This software package is subject to the GNU General Public License
2015-10-22 05:13:26 +00:00
* version 3 or later ( your choice ) as published by the Free Software
2015-09-26 22:11:53 +00:00
* Foundation .
*
2015-10-22 05:13:26 +00:00
* Copyright 2015 Aaron Culliney
2015-09-26 22:11:53 +00:00
*
* /
package org.deadc0de.apple2ix ;
import android.app.AlertDialog ;
import android.content.DialogInterface ;
import android.content.Intent ;
import android.net.Uri ;
import android.os.Build ;
import android.os.Environment ;
import android.util.Log ;
2015-10-24 19:45:52 +00:00
import android.view.View ;
import android.widget.Button ;
import android.widget.ProgressBar ;
2015-09-26 22:11:53 +00:00
import org.deadc0de.apple2ix.basic.BuildConfig ;
import org.deadc0de.apple2ix.basic.R ;
import java.io.BufferedReader ;
import java.io.BufferedWriter ;
import java.io.File ;
import java.io.FileReader ;
import java.io.FileWriter ;
import java.io.FilenameFilter ;
import java.io.IOException ;
import java.io.InterruptedIOException ;
import java.util.concurrent.atomic.AtomicBoolean ;
public class Apple2CrashHandler {
public final static String javaCrashFileName = " jcrash.txt " ;
public static Apple2CrashHandler getInstance ( ) {
return sCrashHandler ;
}
public enum CrashType {
JAVA_CRASH {
@Override
public final String getTitle ( Apple2Activity activity ) {
return activity . getResources ( ) . getString ( R . string . crash_java_npe ) ;
}
} ,
NULL_DEREF {
@Override
public final String getTitle ( Apple2Activity activity ) {
return activity . getResources ( ) . getString ( R . string . crash_null ) ;
}
} ,
STACKCALL_OVERFLOW {
@Override
public final String getTitle ( Apple2Activity activity ) {
return activity . getResources ( ) . getString ( R . string . crash_stackcall_overflow ) ;
}
} ,
STACKBUF_OVERFLOW {
@Override
public final String getTitle ( Apple2Activity activity ) {
return activity . getResources ( ) . getString ( R . string . crash_stackbuf_overflow ) ;
}
} ;
public static final int size = CrashType . values ( ) . length ;
public abstract String getTitle ( Apple2Activity activity ) ;
public static String [ ] titles ( Apple2Activity activity ) {
String [ ] titles = new String [ size ] ;
int i = 0 ;
for ( CrashType setting : values ( ) ) {
titles [ i + + ] = setting . getTitle ( activity ) ;
}
return titles ;
}
}
2015-09-27 21:28:10 +00:00
public synchronized void initializeAndSetCustomExceptionHandler ( Apple2Activity activity ) {
2015-09-27 20:30:16 +00:00
synchronized ( this ) {
if ( homeDir = = null ) {
homeDir = Apple2DisksMenu . getDataDir ( activity ) ;
}
}
2015-09-26 22:11:53 +00:00
if ( mDefaultExceptionHandler ! = null ) {
return ;
}
mDefaultExceptionHandler = Thread . getDefaultUncaughtExceptionHandler ( ) ;
final Thread . UncaughtExceptionHandler defaultExceptionHandler = mDefaultExceptionHandler ;
Thread . setDefaultUncaughtExceptionHandler ( new Thread . UncaughtExceptionHandler ( ) {
@Override
public void uncaughtException ( Thread thread , Throwable t ) {
try {
2015-09-27 20:30:16 +00:00
Apple2CrashHandler . onUncaughtException ( thread , t ) ;
2015-09-26 22:11:53 +00:00
} catch ( Throwable terminator2 ) {
// Yo dawg, I hear you like exceptions in your exception handler! ...
}
defaultExceptionHandler . uncaughtException ( thread , t ) ;
}
} ) ;
}
2015-09-27 20:34:58 +00:00
public void abandonAllHope ( Apple2Activity activity , Throwable nativeBarfed ) {
// write out the early link crash and send this through the main crash processing code
onUncaughtException ( Thread . currentThread ( ) , nativeBarfed ) ;
checkForCrashes ( activity ) ;
}
2015-09-26 22:11:53 +00:00
public boolean areJavaCrashesPresent ( Apple2Activity activity ) {
File javaCrash = _javaCrashFile ( activity ) ;
return javaCrash . exists ( ) ;
}
public boolean areNativeCrashesPresent ( Apple2Activity activity ) {
File [ ] nativeCrashes = _nativeCrashFiles ( activity ) ;
return nativeCrashes ! = null & & nativeCrashes . length > 0 ;
}
public boolean areCrashesPresent ( Apple2Activity activity ) {
return areJavaCrashesPresent ( activity ) | | areNativeCrashesPresent ( activity ) ;
}
2015-09-26 22:20:54 +00:00
public void checkForCrashes ( final Apple2Activity activity ) {
if ( ! areCrashesPresent ( activity ) ) {
return ;
}
2015-10-03 03:17:29 +00:00
if ( ! Apple2Preferences . CRASH_CHECK . booleanValue ( activity ) ) {
return ;
}
2015-09-26 22:20:54 +00:00
boolean previouslyRanCrashCheck = mAlreadyRanCrashCheck . getAndSet ( true ) ;
boolean previouslySentReport = mAlreadySentReport . get ( ) ;
if ( previouslySentReport ) {
// here we assume that the crash data was previously sent via email ... if not then we lost it =P
Log . d ( TAG , " Cleaning up crash data ... " ) ;
int idx = 0 ;
File [ ] nativeCrashes = _nativeCrashFiles ( activity ) ;
for ( File crash : nativeCrashes ) {
if ( ! crash . delete ( ) ) {
Log . d ( TAG , " Could not unlink crash : " + crash ) ;
}
File processed = new File ( _dumpPath2ProcessedPath ( crash . getAbsolutePath ( ) ) ) ;
if ( ! processed . delete ( ) ) {
Log . d ( TAG , " Could not unlink processed : " + processed ) ;
}
}
File javaCrashFile = _javaCrashFile ( activity ) ;
if ( ! javaCrashFile . delete ( ) ) {
Log . d ( TAG , " Could not unlink java crash : " + javaCrashFile ) ;
}
// remove previous log file
2015-10-24 19:45:52 +00:00
_writeTempLogFile ( activity , new StringBuilder ( ) ) ;
2015-09-26 22:20:54 +00:00
return ;
}
if ( previouslyRanCrashCheck ) {
// don't keep asking on return from backgrounding
return ;
}
final AlertDialog crashDialog = new AlertDialog . Builder ( activity ) . setIcon ( R . drawable . ic_launcher ) . setCancelable ( true ) . setTitle ( R . string . crasher_send ) . setMessage ( R . string . crasher_send_message ) . setNegativeButton ( R . string . no , null ) . setPositiveButton ( R . string . ok , new DialogInterface . OnClickListener ( ) {
@Override
public void onClick ( DialogInterface dialog , int which ) {
dialog . dismiss ( ) ;
2015-11-04 06:47:35 +00:00
final Apple2SplashScreen splashScreen = activity . getSplashScreen ( ) ;
if ( splashScreen ! = null ) {
splashScreen . setDismissable ( false ) ;
}
2015-10-24 19:45:52 +00:00
final ProgressBar bar = ( ProgressBar ) activity . findViewById ( R . id . crash_progressBar ) ;
2015-10-24 20:17:47 +00:00
try {
bar . setVisibility ( View . VISIBLE ) ;
} catch ( NullPointerException npe ) {
/* could happen on early lifecycle crashes */
}
2015-10-24 19:45:52 +00:00
new Thread ( new Runnable ( ) {
2015-09-26 22:20:54 +00:00
@Override
public void run ( ) {
2015-10-24 19:45:52 +00:00
android . os . Process . setThreadPriority ( android . os . Process . THREAD_PRIORITY_BACKGROUND ) ;
2015-10-23 06:30:36 +00:00
2015-09-26 22:20:54 +00:00
final int sampleRate = DevicePropertyCalculator . getRecommendedSampleRate ( activity ) ;
final int monoBufferSize = DevicePropertyCalculator . getRecommendedBufferSize ( activity , /*isStereo:*/ false ) ;
final int stereoBufferSize = DevicePropertyCalculator . getRecommendedBufferSize ( activity , /*isStereo:*/ true ) ;
2015-10-30 05:09:36 +00:00
StringBuilder summary = new StringBuilder ( ) ;
2015-09-26 22:20:54 +00:00
StringBuilder allCrashData = new StringBuilder ( ) ;
// prepend information about this device
2015-10-30 05:09:36 +00:00
summary . append ( " BRAND: " ) . append ( Build . BRAND ) . append ( " \ n " ) ;
summary . append ( " MODEL: " ) . append ( Build . MODEL ) . append ( " \ n " ) ;
summary . append ( " MANUFACTURER: " ) . append ( Build . MANUFACTURER ) . append ( " \ n " ) ;
summary . append ( " DEVICE: " ) . append ( Build . DEVICE ) . append ( " \ n " ) ;
summary . append ( " SAMPLE RATE: " ) . append ( sampleRate ) . append ( " \ n " ) ;
summary . append ( " MONO BUFSIZE: " ) . append ( monoBufferSize ) . append ( " \ n " ) ;
summary . append ( " STEREO BUFSIZE: " ) . append ( stereoBufferSize ) . append ( " \ n " ) ;
summary . append ( " GPU VENDOR: " ) . append ( Apple2Preferences . GL_VENDOR . stringValue ( activity ) ) . append ( " \ n " ) ;
summary . append ( " GPU RENDERER: " ) . append ( Apple2Preferences . GL_RENDERER . stringValue ( activity ) ) . append ( " \ n " ) ;
summary . append ( " GPU VERSION: " ) . append ( Apple2Preferences . GL_VERSION . stringValue ( activity ) ) . append ( " \ n " ) ;
allCrashData . append ( summary ) ;
2015-09-26 22:20:54 +00:00
File [ ] nativeCrashes = _nativeCrashFiles ( activity ) ;
if ( nativeCrashes = = null ) {
nativeCrashes = new File [ 0 ] ;
}
2015-10-24 19:45:52 +00:00
final int len = nativeCrashes . length + 1 /* maybe Java crash */ + 1 /* exposeSymbols */ ;
activity . runOnUiThread ( new Runnable ( ) {
@Override
public void run ( ) {
2015-10-24 20:17:47 +00:00
if ( bar ! = null ) {
bar . setMax ( len ) ;
}
2015-10-24 19:45:52 +00:00
}
} ) ;
if ( len > 0 ) {
Apple2DisksMenu . exposeSymbols ( activity ) ;
}
activity . runOnUiThread ( new Runnable ( ) {
@Override
public void run ( ) {
2015-10-24 20:17:47 +00:00
if ( bar ! = null ) {
bar . setProgress ( 1 ) ;
}
2015-10-24 19:45:52 +00:00
}
} ) ;
2015-10-30 05:09:36 +00:00
boolean summarizedHeader = false ;
2015-09-26 22:20:54 +00:00
// iteratively process native crashes
for ( File crash : nativeCrashes ) {
String crashPath = crash . getAbsolutePath ( ) ;
Log . d ( TAG , " Processing crash : " + crashPath ) ;
String processedPath = _dumpPath2ProcessedPath ( crashPath ) ;
2015-10-24 20:17:47 +00:00
try {
nativeProcessCrash ( crashPath , processedPath ) ; // Run Breakpad minidump_stackwalk
} catch ( UnsatisfiedLinkError ule ) {
/* could happen on early lifecycle crashes */
}
2015-09-26 22:20:54 +00:00
StringBuilder crashData = new StringBuilder ( ) ;
if ( ! _readFile ( new File ( processedPath ) , crashData ) ) {
Log . e ( TAG , " Error processing crash : " + crashPath ) ;
}
allCrashData . append ( " >>>>>>> NATIVE CRASH [ " ) . append ( crashPath ) . append ( " ] \ n " ) ;
allCrashData . append ( crashData ) ;
2015-10-30 05:09:36 +00:00
summary . append ( " NATIVE CRASH: \ n " ) ;
// append succinct information about crashing thread
String [ ] lines = crashData . toString ( ) . split ( " [ \\ n \\ r][ \\ n \\ r]* " ) ;
for ( int i = 0 , j = 0 ; i < lines . length ; i + + ) {
// 2 lines of minidump summary
if ( i < 2 ) {
if ( ! summarizedHeader ) {
summary . append ( lines [ i ] ) ;
summary . append ( " \ n " ) ;
}
continue ;
}
// 1 line of crashing thread and reason
if ( i = = 2 ) {
summarizedHeader = true ;
summary . append ( lines [ i ] ) ;
summary . append ( " \ n " ) ;
continue ;
}
// whole lotta modules
if ( lines [ i ] . startsWith ( " Module " ) ) {
continue ;
}
// one apparently empty line
if ( lines [ i ] . matches ( " ^[ \\ t]*$ " ) ) {
continue ;
}
// append crashing thread backtrace
summary . append ( lines [ i ] ) ;
summary . append ( " \ n " ) ;
final int maxSummaryBacktrace = 8 ;
if ( j + + > = maxSummaryBacktrace ) {
break ;
}
}
2015-10-24 19:45:52 +00:00
activity . runOnUiThread ( new Runnable ( ) {
@Override
public void run ( ) {
2015-10-24 20:17:47 +00:00
if ( bar ! = null ) {
bar . setProgress ( bar . getProgress ( ) + 1 ) ;
}
2015-10-24 19:45:52 +00:00
}
} ) ;
2015-09-26 22:20:54 +00:00
}
StringBuilder javaCrashData = new StringBuilder ( ) ;
File javaCrashFile = _javaCrashFile ( activity ) ;
if ( javaCrashFile . exists ( ) ) {
Log . d ( TAG , " Reading java crashes file " ) ;
if ( ! _readFile ( javaCrashFile , javaCrashData ) ) {
Log . e ( TAG , " Error processing java crash : " + javaCrashFileName ) ;
}
}
allCrashData . append ( " >>>>>>> JAVA CRASH DATA \ n " ) ;
allCrashData . append ( javaCrashData ) ;
2015-10-30 05:09:36 +00:00
summary . append ( " JAVA CRASH: \ n " ) ;
summary . append ( javaCrashData ) ;
2015-10-24 19:45:52 +00:00
activity . runOnUiThread ( new Runnable ( ) {
@Override
public void run ( ) {
2015-10-24 20:17:47 +00:00
if ( bar ! = null ) {
bar . setProgress ( bar . getProgress ( ) + 1 ) ;
}
2015-10-24 19:45:52 +00:00
}
} ) ;
2015-09-26 22:20:54 +00:00
2015-10-23 06:30:36 +00:00
Apple2DisksMenu . unexposeSymbols ( activity ) ;
2015-10-24 19:45:52 +00:00
activity . runOnUiThread ( new Runnable ( ) {
@Override
public void run ( ) {
2015-10-24 20:17:47 +00:00
try {
bar . setVisibility ( View . INVISIBLE ) ;
2015-11-04 06:47:35 +00:00
splashScreen . setDismissable ( true ) ;
2015-10-24 20:17:47 +00:00
} catch ( NullPointerException npe ) {
/* could happen on early lifecycle crashes */
}
2015-10-24 19:45:52 +00:00
}
} ) ;
2015-10-23 06:30:36 +00:00
2015-09-26 22:20:54 +00:00
// send report with all the data
2015-10-30 05:09:36 +00:00
_sendEmailToDeveloperWithCrashData ( activity , summary , allCrashData ) ;
2015-09-26 22:20:54 +00:00
}
2015-10-24 19:45:52 +00:00
} ) . start ( ) ;
2015-09-26 22:20:54 +00:00
}
} ) . create ( ) ;
activity . registerAndShowDialog ( crashDialog ) ;
}
2015-09-26 22:11:53 +00:00
public void performCrash ( int crashType ) {
if ( BuildConfig . DEBUG ) {
nativePerformCrash ( crashType ) ;
}
}
// ------------------------------------------------------------------------
// privates
private Apple2CrashHandler ( ) {
/* ... */
}
2015-09-27 20:30:16 +00:00
private static void onUncaughtException ( Thread thread , Throwable t ) {
StackTraceElement [ ] stackTraceElements = t . getStackTrace ( ) ;
StringBuffer traceBuffer = new StringBuffer ( ) ;
// append the Java stack trace
traceBuffer . append ( " NAME: " ) . append ( t . getClass ( ) . getName ( ) ) . append ( " \ n " ) ;
traceBuffer . append ( " MESSAGE: " ) . append ( t . getMessage ( ) ) . append ( " \ n " ) ;
final int maxTraceSize = 2048 + 1024 + 512 ; // probably should keep this less than a standard Linux PAGE_SIZE
for ( StackTraceElement elt : stackTraceElements ) {
traceBuffer . append ( elt . toString ( ) ) ;
traceBuffer . append ( " \ n " ) ;
if ( traceBuffer . length ( ) > = maxTraceSize ) {
break ;
}
}
traceBuffer . append ( " \ n " ) ;
final int maxAttempts = 5 ;
int attempts = 0 ;
do {
try {
BufferedWriter writer = new BufferedWriter ( new FileWriter ( new File ( sCrashHandler . homeDir , javaCrashFileName ) , /*append:*/ true ) ) ;
writer . append ( traceBuffer ) ;
writer . flush ( ) ;
writer . close ( ) ;
break ;
} catch ( InterruptedIOException ie ) {
/* EINTR, EAGAIN ... */
} catch ( IOException e ) {
Log . e ( TAG , " Exception attempting to write data : " + e ) ;
}
try {
Thread . sleep ( 100 , 0 ) ;
} catch ( InterruptedException e ) {
/* ... */
}
+ + attempts ;
} while ( attempts < maxAttempts ) ;
}
2015-09-26 22:11:53 +00:00
private File _javaCrashFile ( Apple2Activity activity ) {
2015-09-27 20:30:16 +00:00
return new File ( homeDir , javaCrashFileName ) ;
2015-09-26 22:11:53 +00:00
}
private File [ ] _nativeCrashFiles ( Apple2Activity activity ) {
FilenameFilter dmpFilter = new FilenameFilter ( ) {
public boolean accept ( File dir , String name ) {
File file = new File ( dir , name ) ;
if ( file . isDirectory ( ) ) {
return false ;
}
// check file extensions ... sigh ... no String.endsWithIgnoreCase() ?
final String extension = " .dmp " ;
final int nameLen = name . length ( ) ;
final int extLen = extension . length ( ) ;
if ( nameLen < = extLen ) {
return false ;
}
String suffix = name . substring ( nameLen - extLen , nameLen ) ;
return ( suffix . equalsIgnoreCase ( extension ) ) ;
}
} ;
2015-09-27 20:30:16 +00:00
return new File ( homeDir ) . listFiles ( dmpFilter ) ;
2015-09-26 22:11:53 +00:00
}
private String _dumpPath2ProcessedPath ( String crashPath ) {
return crashPath . substring ( 0 , crashPath . length ( ) - 4 ) + " .txt " ;
}
private boolean _readFile ( File file , StringBuilder fileData ) {
final int maxAttempts = 5 ;
int attempts = 0 ;
do {
try {
BufferedReader reader = new BufferedReader ( new FileReader ( file ) ) ;
char [ ] buf = new char [ 1024 ] ;
int numRead = 0 ;
while ( ( numRead = reader . read ( buf ) ) ! = - 1 ) {
String readData = String . valueOf ( buf , 0 , numRead ) ;
fileData . append ( readData ) ;
}
reader . close ( ) ;
break ;
} catch ( InterruptedIOException ie ) {
/* EINTR, EAGAIN ... */
} catch ( IOException e ) {
Log . d ( TAG , " Error reading file at path : " + file . toString ( ) ) ;
}
try {
Thread . sleep ( 100 , 0 ) ;
} catch ( InterruptedException e ) {
/* ... */
}
2015-09-27 20:30:16 +00:00
+ + attempts ;
2015-09-26 22:11:53 +00:00
} while ( attempts < maxAttempts ) ;
return attempts < maxAttempts ;
}
2015-10-24 19:45:52 +00:00
private File _writeTempLogFile ( Apple2Activity activity , StringBuilder allCrashData ) {
2015-09-26 22:11:53 +00:00
File allCrashFile = null ;
String storageState = Environment . getExternalStorageState ( ) ;
if ( storageState . equals ( Environment . MEDIA_MOUNTED ) ) {
allCrashFile = new File ( Environment . getExternalStorageDirectory ( ) , " apple2ix_crash.txt " ) ;
} else {
2015-10-24 19:45:52 +00:00
allCrashFile = new File ( Apple2DisksMenu . getDataDir ( activity ) , " apple2ix_crash.txt " ) ;
2015-09-26 22:11:53 +00:00
}
2015-10-24 19:45:52 +00:00
Log . d ( TAG , " Writing all crashes to temp file : " + allCrashFile . getAbsolutePath ( ) ) ;
2015-09-26 22:11:53 +00:00
final int maxAttempts = 5 ;
int attempts = 0 ;
do {
try {
BufferedWriter writer = new BufferedWriter ( new FileWriter ( allCrashFile ) ) ;
writer . append ( allCrashData ) ;
writer . flush ( ) ;
writer . close ( ) ;
break ;
} catch ( InterruptedIOException ie ) {
/* EINTR, EAGAIN ... */
} catch ( IOException e ) {
Log . e ( TAG , " Exception attempting to write data : " + e ) ;
}
try {
Thread . sleep ( 100 , 0 ) ;
} catch ( InterruptedException e ) {
/* ... */
}
+ + attempts ;
} while ( attempts < maxAttempts ) ;
2015-10-24 19:45:52 +00:00
if ( ! allCrashFile . setReadable ( true , /*ownerOnly:*/ false ) ) {
Log . d ( TAG , " Oops, could not set all crash data readable! " ) ;
}
2015-09-26 22:11:53 +00:00
return allCrashFile ;
}
2015-10-30 05:09:36 +00:00
private void _sendEmailToDeveloperWithCrashData ( Apple2Activity activity , StringBuilder summary , StringBuilder allCrashData ) {
2015-09-26 22:11:53 +00:00
mAlreadySentReport . set ( true ) ;
2015-10-30 05:09:36 +00:00
// <sigh> ... the disaster that is early Android ... there does not appear to be a reliable way to start an
// email Intent to send both text and an attachment, but we make a valiant (if futile) effort to do so here.
// And the reason to send an attachment is that you trigger an android.os.TransactionTooLargeException with too
// much text data in the EXTRA_TEXT ... </sigh>
2015-09-26 22:11:53 +00:00
Intent emailIntent = new Intent ( Intent . ACTION_SENDTO , Uri . fromParts ( " mailto " , " apple2ix_crash@deadcode.org " /*non-zero variant is correct endpoint at the moment*/ , null ) ) ;
emailIntent . putExtra ( Intent . EXTRA_SUBJECT , " Crasher " ) ;
2015-10-30 05:09:36 +00:00
final int maxCharsEmail = 4096 ;
int len = summary . length ( ) ;
len = len < maxCharsEmail ? len : maxCharsEmail ;
String summaryData = summary . substring ( 0 , len ) ;
emailIntent . putExtra ( Intent . EXTRA_TEXT , " The app crashed, please help! \ n \ n " + summaryData ) ;
2015-10-24 19:45:52 +00:00
File allCrashFile = _writeTempLogFile ( activity , allCrashData ) ;
2015-09-26 22:11:53 +00:00
emailIntent . putExtra ( Intent . EXTRA_STREAM , Uri . fromFile ( allCrashFile ) ) ;
Log . d ( TAG , " STARTING CHOOSER FOR EMAIL ... " ) ;
activity . startActivity ( Intent . createChooser ( emailIntent , " Send email " ) ) ;
Log . d ( TAG , " AFTER START ACTIVITY ... " ) ;
}
private final static String TAG = " Apple2CrashHandler " ;
private final static Apple2CrashHandler sCrashHandler = new Apple2CrashHandler ( ) ;
2015-09-27 20:30:16 +00:00
private String homeDir ;
2015-09-26 22:11:53 +00:00
private Thread . UncaughtExceptionHandler mDefaultExceptionHandler ;
private AtomicBoolean mAlreadyRanCrashCheck = new AtomicBoolean ( false ) ;
private AtomicBoolean mAlreadySentReport = new AtomicBoolean ( false ) ;
private static native void nativePerformCrash ( int crashType ) ; // testing
2015-09-26 22:20:54 +00:00
private static native void nativeProcessCrash ( String crashFilePath , String crashProcessedPath ) ;
2015-09-26 22:11:53 +00:00
}