From 88be6086a2909c4a840a8a24575e44a488bed106 Mon Sep 17 00:00:00 2001 From: Aaron Culliney Date: Sat, 26 Sep 2015 15:11:53 -0700 Subject: [PATCH] Refactor crash testing/reports into separate class/files --- .../org/deadc0de/apple2ix/Apple2Activity.java | 77 +---- .../deadc0de/apple2ix/Apple2CrashHandler.java | 284 ++++++++++++++++++ .../deadc0de/apple2ix/Apple2SettingsMenu.java | 15 +- Android/app/src/main/res/values/strings.xml | 11 +- Android/jni/jnicrash.c | 39 ++- Android/jni/jnihooks.c | 35 --- 6 files changed, 340 insertions(+), 121 deletions(-) create mode 100644 Android/app/src/main/java/org/deadc0de/apple2ix/Apple2CrashHandler.java diff --git a/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2Activity.java b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2Activity.java index 5a26a3fd..fecb6c81 100644 --- a/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2Activity.java +++ b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2Activity.java @@ -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); diff --git a/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2CrashHandler.java b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2CrashHandler.java new file mode 100644 index 00000000..583cd025 --- /dev/null +++ b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2CrashHandler.java @@ -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); + +} diff --git a/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2SettingsMenu.java b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2SettingsMenu.java index a4f2f932..21c4856a 100644 --- a/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2SettingsMenu.java +++ b/Android/app/src/main/java/org/deadc0de/apple2ix/Apple2SettingsMenu.java @@ -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 } } }; diff --git a/Android/app/src/main/res/values/strings.xml b/Android/app/src/main/res/values/strings.xml index 7f18feee..d13b22a8 100644 --- a/Android/app/src/main/res/values/strings.xml +++ b/Android/app/src/main/res/values/strings.xml @@ -19,11 +19,14 @@ Color Interpolated color Crasher - Crash emulator + Check for crash reports + Check for crash reports to email developer) + Processing… + Processing crash reports… + Send crash report? + Do you want to send a crash report to the developer? Test crash generation - Send crash reports - Send crash reports - Will email crash reports to developer (if any) + Crash emulator NULL-deref Java NPE stack call overflow diff --git a/Android/jni/jnicrash.c b/Android/jni/jnicrash.c index 027dd3d7..db4fdd5d 100644 --- a/Android/jni/jnicrash.c +++ b/Android/jni/jnicrash.c @@ -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)); +} + diff --git a/Android/jni/jnihooks.c b/Android/jni/jnihooks.c index 8f34bc54..9f900cfd 100644 --- a/Android/jni/jnihooks.c +++ b/Android/jni/jnihooks.c @@ -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)) {