Refactor crash testing/reports into separate class/files

This commit is contained in:
Aaron Culliney 2015-09-26 15:11:53 -07:00
parent a20b2461b5
commit 88be6086a2
6 changed files with 340 additions and 121 deletions

View File

@ -39,8 +39,6 @@ public class Apple2Activity extends Activity {
private final static int MAX_FINGERS = 32;// HACK ...
private static volatile boolean DEBUG_STRICT = false;
private boolean mSetUncaughtExceptionHandler = false;
private Apple2View mView = null;
private Apple2SplashScreen mSplashScreen = null;
private Apple2MainMenu mMainMenu = null;
@ -52,10 +50,6 @@ public class Apple2Activity extends Activity {
private int mWidth = 0;
private int mHeight = 0;
private int mSampleRate = 0;
private int mMonoBufferSize = 0;
private int mStereoBufferSize = 0;
private float[] mXCoords = new float[MAX_FINGERS];
private float[] mYCoords = new float[MAX_FINGERS];
@ -91,8 +85,6 @@ public class Apple2Activity extends Activity {
private native void nativeOnKeyUp(int keyCode, int metaState);
private native void nativeOnUncaughtException(String home, String trace);
private native void nativeOnResume(boolean isSystemResume);
public native void nativeOnPause(boolean isSystemPause);
@ -112,63 +104,6 @@ public class Apple2Activity extends Activity {
public native void nativePerformCrash(int crashType);
private void _setCustomExceptionHandler() {
if (mSetUncaughtExceptionHandler) {
return;
}
mSetUncaughtExceptionHandler = true;
final String homeDir = "/data/data/" + this.getPackageName();
final Thread.UncaughtExceptionHandler defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread thread, Throwable t) {
try {
StackTraceElement[] stackTraceElements = t.getStackTrace();
StringBuffer traceBuffer = new StringBuffer();
// prepend information about this device
traceBuffer.append(Build.BRAND);
traceBuffer.append("\n");
traceBuffer.append(Build.MODEL);
traceBuffer.append("\n");
traceBuffer.append(Build.MANUFACTURER);
traceBuffer.append("\n");
traceBuffer.append(Build.DEVICE);
traceBuffer.append("\n");
traceBuffer.append("Device sample rate:");
traceBuffer.append(mSampleRate);
traceBuffer.append("\n");
traceBuffer.append("Device mono buffer size:");
traceBuffer.append(mMonoBufferSize);
traceBuffer.append("\n");
traceBuffer.append("Device stereo buffer size:");
traceBuffer.append(mStereoBufferSize);
traceBuffer.append("\n");
// now append the actual stack trace
traceBuffer.append(t.getClass().getName());
traceBuffer.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");
nativeOnUncaughtException(homeDir, traceBuffer.toString());
} catch (Throwable terminator2) {
// Yo dawg, I hear you like exceptions in your exception handler! ...
}
defaultExceptionHandler.uncaughtException(thread, t);
}
});
}
@Override
public void onCreate(Bundle savedInstanceState) {
if (Apple2Activity.DEBUG_STRICT && BuildConfig.DEBUG) {
@ -189,7 +124,7 @@ public class Apple2Activity extends Activity {
Log.e(TAG, "onCreate()");
_setCustomExceptionHandler();
Apple2CrashHandler.getInstance().setCustomExceptionHandler(this);
// run first-time initializations
if (!Apple2Preferences.FIRST_TIME_CONFIGURED.booleanValue(this)) {
@ -199,13 +134,13 @@ public class Apple2Activity extends Activity {
Apple2Preferences.FIRST_TIME_CONFIGURED.saveBoolean(this, true);
// get device audio parameters for native OpenSLES
mSampleRate = DevicePropertyCalculator.getRecommendedSampleRate(this);
mMonoBufferSize = DevicePropertyCalculator.getRecommendedBufferSize(this, /*isStereo:*/false);
mStereoBufferSize = DevicePropertyCalculator.getRecommendedBufferSize(this, /*isStereo:*/true);
Log.d(TAG, "Device sampleRate:" + mSampleRate + " mono bufferSize:" + mMonoBufferSize + " stereo bufferSize:" + mStereoBufferSize);
int sampleRate = DevicePropertyCalculator.getRecommendedSampleRate(this);
int monoBufferSize = DevicePropertyCalculator.getRecommendedBufferSize(this, /*isStereo:*/false);
int stereoBufferSize = DevicePropertyCalculator.getRecommendedBufferSize(this, /*isStereo:*/true);
Log.d(TAG, "Device sampleRate:" + sampleRate + " mono bufferSize:" + monoBufferSize + " stereo bufferSize:" + stereoBufferSize);
String dataDir = Apple2DisksMenu.getDataDir(this);
nativeOnCreate(dataDir, mSampleRate, mMonoBufferSize, mStereoBufferSize);
nativeOnCreate(dataDir, sampleRate, monoBufferSize, stereoBufferSize);
// NOTE: load preferences after nativeOnCreate ... native CPU thread should still be paused
Apple2Preferences.loadPreferences(this);

View File

@ -0,0 +1,284 @@
/*
* Apple // emulator for *nix
*
* This software package is subject to the GNU General Public License
* version 2 or later (your choice) as published by the Free Software
* Foundation.
*
* THERE ARE NO WARRANTIES WHATSOEVER.
*
*/
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.os.Handler;
import android.util.Log;
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;
}
}
public synchronized void setCustomExceptionHandler(Apple2Activity activity) {
if (mDefaultExceptionHandler != null) {
return;
}
mDefaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
final String homeDir = "/data/data/" + activity.getPackageName();
final Thread.UncaughtExceptionHandler defaultExceptionHandler = mDefaultExceptionHandler;
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread thread, Throwable t) {
try {
StackTraceElement[] stackTraceElements = t.getStackTrace();
StringBuffer traceBuffer = new StringBuffer();
// append the Java stack trace
traceBuffer.append(t.getClass().getName());
traceBuffer.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");
nativeOnUncaughtException(homeDir, traceBuffer.toString());
} catch (Throwable terminator2) {
// Yo dawg, I hear you like exceptions in your exception handler! ...
}
defaultExceptionHandler.uncaughtException(thread, t);
}
});
}
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);
}
public void performCrash(int crashType) {
if (BuildConfig.DEBUG) {
nativePerformCrash(crashType);
}
}
// ------------------------------------------------------------------------
// privates
private Apple2CrashHandler() {
/* ... */
}
private File _javaCrashFile(Apple2Activity activity) {
return new File(Apple2DisksMenu.getDataDir(activity), javaCrashFileName);
}
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));
}
};
return new File(Apple2DisksMenu.getDataDir(activity)).listFiles(dmpFilter);
}
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) {
/* ... */
}
++ attempts;
} while (attempts < maxAttempts);
return attempts < maxAttempts;
}
private File _writeTempLogFile(StringBuilder allCrashData) {
File allCrashFile = null;
String storageState = Environment.getExternalStorageState();
if (storageState.equals(Environment.MEDIA_MOUNTED)) {
allCrashFile = new File(Environment.getExternalStorageDirectory(), "apple2ix_crash.txt");
} else {
allCrashFile = new File("/data/local/tmp", "apple2ix_crash.txt");
}
Log.d(TAG, "Writing all crashes to temp file : " + allCrashFile);
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);
return allCrashFile;
}
private void _sendEmailToDeveloperWithCrashData(Apple2Activity activity, StringBuilder allCrashData) {
mAlreadySentReport.set(true);
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");
File allCrashFile = _writeTempLogFile(allCrashData);
// Putting all the text data into the EXTRA_TEXT appears to trigger android.os.TransactionTooLargeException ...
//emailIntent.putExtra(Intent.EXTRA_TEXT, allCrashData.toString());
emailIntent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(allCrashFile));
// But we can put some text data
emailIntent.putExtra(Intent.EXTRA_TEXT, "Greeting Apple2ix developers! The app crashed, please help!");
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();
private Thread.UncaughtExceptionHandler mDefaultExceptionHandler;
private AtomicBoolean mAlreadyRanCrashCheck = new AtomicBoolean(false);
private AtomicBoolean mAlreadySentReport = new AtomicBoolean(false);
private static native void nativePerformCrash(int crashType); // testing
private static native void nativeOnUncaughtException(String home, String trace);
}

View File

@ -300,7 +300,7 @@ public class Apple2SettingsMenu extends Apple2AbstractMenu {
// in debug mode we actually exercise the crash reporter ...
return activity.getResources().getString(R.string.crasher_title);
} else {
return activity.getResources().getString(R.string.crasher_send_title);
return activity.getResources().getString(R.string.crasher_check_title);
}
}
@ -309,7 +309,7 @@ public class Apple2SettingsMenu extends Apple2AbstractMenu {
if (BuildConfig.DEBUG) {
return activity.getResources().getString(R.string.crasher_summary);
} else {
return activity.getResources().getString(R.string.crasher_send_summary);
return activity.getResources().getString(R.string.crasher_check_summary);
}
}
@ -317,12 +317,7 @@ public class Apple2SettingsMenu extends Apple2AbstractMenu {
public void handleSelection(final Apple2Activity activity, final Apple2AbstractMenu settingsMenu, boolean isChecked) {
if (BuildConfig.DEBUG) {
_alertDialogHandleSelection(activity, R.string.crasher, new String[]{
activity.getResources().getString(R.string.crash_java_npe),
activity.getResources().getString(R.string.crash_null),
activity.getResources().getString(R.string.crash_stackcall_overflow),
activity.getResources().getString(R.string.crash_stackbuf_overflow),
}, new IPreferenceLoadSave() {
_alertDialogHandleSelection(activity, R.string.crasher, Apple2CrashHandler.CrashType.titles(activity), new IPreferenceLoadSave() {
@Override
public int intValue() {
return -1;
@ -344,13 +339,13 @@ public class Apple2SettingsMenu extends Apple2AbstractMenu {
break;
default:
activity.nativePerformCrash(value);
Apple2CrashHandler.getInstance().performCrash(value);
break;
}
}
});
} else {
// TODO FIXME : run local crash analysis and open Email Intent to send
// TODO FIXME : checkbox on whether to enable checking/sending crashes
}
}
};

View File

@ -19,11 +19,14 @@
<string name="color_color">Color</string>
<string name="color_interpolated">Interpolated color</string>
<string name="crasher">Crasher</string>
<string name="crasher_title">Crash emulator</string>
<string name="crasher_check_title">Check for crash reports</string>
<string name="crasher_check_summary">Check for crash reports to email developer)</string>
<string name="crasher_processing">Processing…</string>
<string name="crasher_processing_message">Processing crash reports…</string>
<string name="crasher_send">Send crash report?</string>
<string name="crasher_send_message">Do you want to send a crash report to the developer?</string>
<string name="crasher_summary">Test crash generation</string>
<string name="crasher_send">Send crash reports</string>
<string name="crasher_send_title">Send crash reports</string>
<string name="crasher_send_summary">Will email crash reports to developer (if any)</string>
<string name="crasher_title">Crash emulator</string>
<string name="crash_null">NULL-deref</string>
<string name="crash_java_npe">Java NPE</string>
<string name="crash_stackcall_overflow">stack call overflow</string>

View File

@ -68,7 +68,8 @@ static volatile int __attribute__((noinline)) _crash_stackbuf_overflow(void) {
return getpid();
}
void Java_org_deadc0de_apple2ix_Apple2Activity_nativePerformCrash(JNIEnv *env, jobject obj, jint crashType) {
void Java_org_deadc0de_apple2ix_Apple2CrashHandler_nativePerformCrash(JNIEnv *env, jclass cls, jint crashType) {
#warning FIXME TODO ... we should turn off test codepaths in release build =D
LOG("... performing crash of type : %d", crashType);
switch (crashType) {
@ -91,3 +92,39 @@ void Java_org_deadc0de_apple2ix_Apple2Activity_nativePerformCrash(JNIEnv *env, j
}
}
#define _JAVA_CRASH_NAME "/jcrash.txt" // this should match the Java side
#define _HALF_PAGE_SIZE (PAGE_SIZE>>1)
void Java_org_deadc0de_apple2ix_Apple2CrashHandler_nativeOnUncaughtException(JNIEnv *env, jclass cls, jstring jhome, jstring jstr) {
RELEASE_ERRLOG("Uncaught Java Exception ...");
// Write to /data/data/org.deadc0de.apple2ix.basic/jcrash.txt
const char *home = (*env)->GetStringUTFChars(env, jhome, NULL);
char *q = (char *)home;
char buf[_HALF_PAGE_SIZE] = { 0 };
const char *p0 = &buf[0];
char *p = (char *)p0;
while (*q && (p-p0 < _HALF_PAGE_SIZE-1)) {
*p++ = *q++;
}
(*env)->ReleaseStringUTFChars(env, jhome, home);
q = &_JAVA_CRASH_NAME[0];
while (*q && (p-p0 < _HALF_PAGE_SIZE-1)) {
*p++ = *q++;
}
int fd = TEMP_FAILURE_RETRY(open(buf, (O_CREAT|O_APPEND|O_WRONLY), (S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)));
if (fd == -1) {
RELEASE_ERRLOG("OOPS, could not create/write to java crash file");
return;
}
const char *str = (*env)->GetStringUTFChars(env, jstr, NULL);
jsize len = (*env)->GetStringUTFLength(env, jstr);
TEMP_FAILURE_RETRY(write(fd, str, len));
(*env)->ReleaseStringUTFChars(env, jstr, str);
TEMP_FAILURE_RETRY(fsync(fd));
TEMP_FAILURE_RETRY(close(fd));
}

View File

@ -252,41 +252,6 @@ void Java_org_deadc0de_apple2ix_Apple2Activity_nativeOnQuit(JNIEnv *env, jobject
#endif
}
#define _JAVA_CRASH_NAME "/jcrash.txt"
#define _HALF_PAGE_SIZE (PAGE_SIZE>>1)
void Java_org_deadc0de_apple2ix_Apple2Activity_nativeOnUncaughtException(JNIEnv *env, jobject obj, jstring jhome, jstring jstr) {
RELEASE_ERRLOG("Uncaught Java Exception ...");
// Write to /data/data/org.deadc0de.apple2ix.basic/jcrash.txt
const char *home = (*env)->GetStringUTFChars(env, jhome, NULL);
char *q = (char *)home;
char buf[_HALF_PAGE_SIZE] = { 0 };
const char *p0 = &buf[0];
char *p = (char *)p0;
while (*q && (p-p0 < _HALF_PAGE_SIZE-1)) {
*p++ = *q++;
}
(*env)->ReleaseStringUTFChars(env, jhome, home);
q = &_JAVA_CRASH_NAME[0];
while (*q && (p-p0 < _HALF_PAGE_SIZE-1)) {
*p++ = *q++;
}
int fd = TEMP_FAILURE_RETRY(open(buf, (O_CREAT|O_APPEND|O_WRONLY), (S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)));
if (fd == -1) {
RELEASE_ERRLOG("OOPS, could not create/write to java crash file");
return;
}
const char *str = (*env)->GetStringUTFChars(env, jstr, NULL);
jsize len = (*env)->GetStringUTFLength(env, jstr);
TEMP_FAILURE_RETRY(write(fd, str, len));
(*env)->ReleaseStringUTFChars(env, jstr, str);
TEMP_FAILURE_RETRY(fsync(fd));
TEMP_FAILURE_RETRY(close(fd));
}
void Java_org_deadc0de_apple2ix_Apple2Activity_nativeOnKeyDown(JNIEnv *env, jobject obj, jint keyCode, jint metaState) {
if (UNLIKELY(shuttingDown)) {