Bug 1263110 - Part 2 - Implement crash loop detection in Java. r=mfinkle,sebastian draft
authorJan Henning <jh+bugzilla@buttercookie.de>
Tue, 19 Apr 2016 13:51:06 +0200
changeset 356905 c2f238d98a3808c69fa403d7ec8146b9213ae278
parent 356904 c4d2c082f319e119d9821177d3d9b3dc9b659591
child 356906 a39c8987e95a9bc690302327c1cfb7dd3144ddfc
push id16637
push usermozilla@buttercookie.de
push dateWed, 27 Apr 2016 13:50:52 +0000
reviewersmfinkle, sebastian
bugs1263110, 701092
milestone49.0a1
Bug 1263110 - Part 2 - Implement crash loop detection in Java. r=mfinkle,sebastian Bug 701092 originally implemented some functionality to detect successive crashes and then turn off session restore for the next start, however that functionality got lost when parts of the startup session restore code were moved to Java. This patch re-implements this functionality within the Java UI. Unlike the previous implementation, we don't reset the crash counter in onPause(), because often enough onPause() will execute even after a crash. Instead, we check in onResume() whether our last foreground activity cycle crashed or not. To avoid cross-process writes and reads to shared preferences, the crash reporter no longer sets the relevant flags in GeckoApp's shared prefs directly, but instead writes an empty CRASHED file to the Mozilla directory as a flag, which is then checked for by the main process during startup. Alternative solutions considered were: - Using Context.MODE_MULTI_PROCESS for accessing the shared prefs. Works, but forces the shared preferences to always be re-read from storage and is also deprecated from API 23 onwards. - Using a ContentProvider for managing the cross-process shared prefs as suggested in Google's documentation. Seems somewhat over-engineered for this use case. - Sending a broadcast from the crash reporter to signal the main process, so it can update the relevant shared prefs from the correct process. Doesn't work reliably immediately after crashing - sometimes the broadcast never arrives. - Setting the crash flags directly in the crash handling functions in GeckoAppShell. Could work even when not building the crash reporter, however doesn't work easily for native crashes, because those are handled internally by Gecko without going through the Java crash handling code. MozReview-Commit-ID: 6g7AmnJhoQk
mobile/android/base/java/org/mozilla/gecko/CrashReporter.java
mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
mobile/android/components/SessionStore.js
--- a/mobile/android/base/java/org/mozilla/gecko/CrashReporter.java
+++ b/mobile/android/base/java/org/mozilla/gecko/CrashReporter.java
@@ -128,22 +128,23 @@ public class CrashReporter extends AppCo
 
         File extrasFile = new File(passedMinidumpPath.replaceAll("\\.dmp", ".extra"));
         mPendingExtrasFile = new File(pendingDir, extrasFile.getName());
         moveFile(extrasFile, mPendingExtrasFile);
 
         mExtrasStringMap = new HashMap<String, String>();
         readStringsFromFile(mPendingExtrasFile.getPath(), mExtrasStringMap);
 
-        // Set the flag that indicates we were stopped as expected, as
-        // we will send a crash report, so it is not a silent OOM crash.
-        SharedPreferences.Editor appPrefsEditor = GeckoSharedPrefs.forApp(this).edit();
-        appPrefsEditor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, true);
-        appPrefsEditor.putBoolean(GeckoApp.PREFS_CRASHED, true);
-        appPrefsEditor.apply();
+        // Notify GeckoApp that we've crashed, so it can react appropriately during the next start.
+        try {
+            File crashFlag = new File(GeckoProfileDirectories.getMozillaDirectory(this), "CRASHED");
+            crashFlag.createNewFile();
+        } catch (GeckoProfileDirectories.NoMozillaDirectoryException | IOException e) {
+            Log.e(LOGTAG, "Cannot set crash flag: ", e);
+        }
 
         final CheckBox allowContactCheckBox = (CheckBox) findViewById(R.id.allow_contact);
         final CheckBox includeUrlCheckBox = (CheckBox) findViewById(R.id.include_url);
         final CheckBox sendReportCheckBox = (CheckBox) findViewById(R.id.send_report);
         final EditText commentsEditText = (EditText) findViewById(R.id.comment);
         final EditText emailEditText = (EditText) findViewById(R.id.email);
 
         // Load CrashReporter preferences to avoid redundant user input.
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
@@ -1,29 +1,31 @@
 /* -*- 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/. */
 
 package org.mozilla.gecko;
 
 import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.URLMetadataTable;
 import org.mozilla.gecko.db.UrlAnnotations;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
 import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.gfx.FullScreenState;
 import org.mozilla.gecko.gfx.Layer;
 import org.mozilla.gecko.gfx.LayerView;
 import org.mozilla.gecko.gfx.PluginLayer;
 import org.mozilla.gecko.health.HealthRecorder;
 import org.mozilla.gecko.health.SessionInformation;
 import org.mozilla.gecko.health.StubbedHealthRecorder;
+import org.mozilla.gecko.home.HomeConfig.PanelType;
 import org.mozilla.gecko.menu.GeckoMenu;
 import org.mozilla.gecko.menu.GeckoMenuInflater;
 import org.mozilla.gecko.menu.MenuPanel;
 import org.mozilla.gecko.mozglue.ContextUtils;
 import org.mozilla.gecko.mozglue.ContextUtils.SafeIntent;
 import org.mozilla.gecko.mozglue.GeckoLoader;
 import org.mozilla.gecko.permissions.Permissions;
 import org.mozilla.gecko.preferences.ClearOnShutdownPref;
@@ -150,17 +152,17 @@ public abstract class GeckoApp
     public static final String ACTION_INIT_PW              = "org.mozilla.gecko.INIT_PW";
 
     public static final String EXTRA_STATE_BUNDLE          = "stateBundle";
 
     public static final String PREFS_ALLOW_STATE_BUNDLE    = "allowStateBundle";
     public static final String PREFS_OOM_EXCEPTION         = "OOMException";
     public static final String PREFS_VERSION_CODE          = "versionCode";
     public static final String PREFS_WAS_STOPPED           = "wasStopped";
-    public static final String PREFS_CRASHED               = "crashed";
+    public static final String PREFS_CRASHED_COUNT         = "crashedCount";
     public static final String PREFS_CLEANUP_TEMP_FILES    = "cleanupTempFiles";
 
     public static final String SAVED_STATE_IN_BACKGROUND   = "inBackground";
     public static final String SAVED_STATE_PRIVATE_SESSION = "privateSession";
 
     // Delay before running one-time "cleanup" tasks that may be needed
     // after a version upgrade.
     private static final int CLEANUP_DEFERRAL_SECONDS = 15;
@@ -189,16 +191,17 @@ public abstract class GeckoApp
     protected LayerView mLayerView;
     private AbsoluteLayout mPluginContainer;
 
     private FullScreenHolder mFullScreenPluginContainer;
     private View mFullScreenPluginView;
 
     private final HashMap<String, PowerManager.WakeLock> mWakeLocks = new HashMap<String, PowerManager.WakeLock>();
 
+    protected boolean mLastSessionCrashed;
     protected boolean mShouldRestore;
     protected boolean mInitialized;
     protected boolean mWindowFocusInitialized;
     private Telemetry.Timer mJavaUiStartupTimer;
     private Telemetry.Timer mGeckoReadyStartupTimer;
 
     private String mPrivateBrowsingSession;
 
@@ -475,17 +478,17 @@ public abstract class GeckoApp
                 res.put("sanitize", clearObj);
             } catch (JSONException ex) {
                 Log.e(LOGTAG, "Error adding sanitize object", ex);
             }
 
             // If the user has opted out of session restore, and does want to clear history
             // we also want to prevent the current session info from being saved.
             if (clearObj.has("private.data.history")) {
-                final String sessionRestore = getSessionRestorePreference();
+                final String sessionRestore = getSessionRestorePreference(getSharedPreferences());
                 try {
                     res.put("dontSaveSession", "quit".equals(sessionRestore));
                 } catch (JSONException ex) {
                     Log.e(LOGTAG, "Error adding session restore data", ex);
                 }
             }
 
             GeckoAppShell.notifyObservers("Browser:Quit", res.toString());
@@ -1275,16 +1278,17 @@ public abstract class GeckoApp
                     (TextSelectionHandle) findViewById(R.id.caret_handle),
                     (TextSelectionHandle) findViewById(R.id.focus_handle));
         } else {
             mTextSelection = new FloatingToolbarTextSelection(this, mLayerView);
         }
         mTextSelection.create();
 
         // Determine whether we should restore tabs.
+        mLastSessionCrashed = updateCrashedState();
         mShouldRestore = getSessionRestoreState(savedInstanceState);
         if (mShouldRestore && savedInstanceState != null) {
             boolean wasInBackground =
                 savedInstanceState.getBoolean(SAVED_STATE_IN_BACKGROUND, false);
 
             // Don't log OOM-kills if only one activity was destroyed. (For example
             // from "Don't keep activities" on ICS)
             if (!wasInBackground && !mIsRestoringActivity) {
@@ -1407,23 +1411,29 @@ public abstract class GeckoApp
     protected void initializeChrome() {
         mDoorHangerPopup = new DoorHangerPopup(this);
         mPluginContainer = (AbsoluteLayout) findViewById(R.id.plugin_container);
         mFormAssistPopup = (FormAssistPopup) findViewById(R.id.form_assist_popup);
     }
 
     /**
      * Loads the initial tab at Fennec startup. If we don't restore tabs, this
-     * tab will be about:home, or the homepage if the use has set one.
+     * tab will be about:home, or the homepage if the user has set one.
+     * If we've temporarily disabled restoring to break out of a crash loop, we'll
+     * show the recent tabs panel so the user can manually restore tabs as needed.
      * If we restore tabs, we don't need to create a new tab.
      */
     protected void loadStartupTab(final int flags) {
         if (!mShouldRestore) {
-            final String homepage = getHomepage();
-            Tabs.getInstance().loadUrl(!TextUtils.isEmpty(homepage) ? homepage : AboutPages.HOME, flags);
+            if (mLastSessionCrashed) {
+                Tabs.getInstance().loadUrl(AboutPages.getURLForBuiltinPanelType(PanelType.RECENT_TABS), flags);
+            } else {
+                final String homepage = getHomepage();
+                Tabs.getInstance().loadUrl(!TextUtils.isEmpty(homepage) ? homepage : AboutPages.HOME, flags);
+            }
         }
     }
 
     /**
      * Loads the initial tab at Fennec startup. This tab will load with the given
      * external URL. If that URL is invalid, a startup tab will be loaded.
      *
      * @param url    External URL to load.
@@ -1693,47 +1703,90 @@ public abstract class GeckoApp
     }
 
     @Override
     public GeckoProfile getProfile() {
         return GeckoThread.getActiveProfile();
     }
 
     /**
+     * Check whether we've crashed during the last browsing session.
+     *
+     * @return True if the crash reporter ran after the last session.
+     */
+    protected boolean updateCrashedState() {
+        try {
+            File crashFlag = new File(GeckoProfileDirectories.getMozillaDirectory(this), "CRASHED");
+            if (crashFlag.exists() && crashFlag.delete()) {
+                // Set the flag that indicates we were stopped as expected, as
+                // the crash reporter has run, so it is not a silent OOM crash.
+                getSharedPreferences().edit().putBoolean(PREFS_WAS_STOPPED, true).apply();
+                return true;
+            }
+        } catch (NoMozillaDirectoryException e) {
+            // If we can't access the Mozilla directory, we're in trouble anyway.
+            Log.e(LOGTAG, "Cannot read crash flag: ", e);
+        }
+        return false;
+    }
+
+    /**
      * Determine whether the session should be restored.
      *
      * @param savedInstanceState Saved instance state given to the activity
      * @return                   Whether to restore
      */
     protected boolean getSessionRestoreState(Bundle savedInstanceState) {
         final SharedPreferences prefs = getSharedPreferences();
         boolean shouldRestore = false;
 
         final int versionCode = getVersionCode();
-        if (prefs.getInt(PREFS_VERSION_CODE, 0) != versionCode) {
+        if (mLastSessionCrashed) {
+            if (incrementCrashCount(prefs) <= getSessionStoreMaxCrashResumes(prefs) &&
+                    getSessionRestoreAfterCrashPreference(prefs)) {
+                shouldRestore = true;
+            } else {
+                shouldRestore = false;
+            }
+        } else if (prefs.getInt(PREFS_VERSION_CODE, 0) != versionCode) {
             // If the version has changed, the user has done an upgrade, so restore
             // previous tabs.
             prefs.edit().putInt(PREFS_VERSION_CODE, versionCode).apply();
             shouldRestore = true;
         } else if (savedInstanceState != null ||
-                   getSessionRestorePreference().equals("always") ||
+                   getSessionRestorePreference(prefs).equals("always") ||
                    getRestartFromIntent()) {
             // We're coming back from a background kill by the OS, the user
             // has chosen to always restore, or we restarted.
             shouldRestore = true;
-        } else if (prefs.getBoolean(GeckoApp.PREFS_CRASHED, false)) {
-            prefs.edit().putBoolean(PREFS_CRASHED, false).apply();
-            shouldRestore = true;
         }
 
         return shouldRestore;
     }
 
-    private String getSessionRestorePreference() {
-        return getSharedPreferences().getString(GeckoPreferences.PREFS_RESTORE_SESSION, "always");
+    private int incrementCrashCount(SharedPreferences prefs) {
+        final int crashCount = getSuccessiveCrashesCount(prefs) + 1;
+        prefs.edit().putInt(PREFS_CRASHED_COUNT, crashCount).apply();
+        return crashCount;
+    }
+
+    private int getSuccessiveCrashesCount(SharedPreferences prefs) {
+        return prefs.getInt(PREFS_CRASHED_COUNT, 0);
+    }
+
+    private int getSessionStoreMaxCrashResumes(SharedPreferences prefs) {
+        return prefs.getInt(GeckoPreferences.PREFS_RESTORE_SESSION_MAX_CRASH_RESUMES, 1);
+    }
+
+    private boolean getSessionRestoreAfterCrashPreference(SharedPreferences prefs) {
+        return prefs.getBoolean(GeckoPreferences.PREFS_RESTORE_SESSION_FROM_CRASH, true);
+    }
+
+    private String getSessionRestorePreference(SharedPreferences prefs) {
+        return prefs.getString(GeckoPreferences.PREFS_RESTORE_SESSION, "always");
     }
 
     private boolean getRestartFromIntent() {
         return ContextUtils.getBooleanExtra(getIntent(), "didRestart", false);
     }
 
     /**
      * Enable Android StrictMode checks (for supported OS versions).
@@ -1957,16 +2010,23 @@ public abstract class GeckoApp
             public void run() {
                 // Now construct the new session on HealthRecorder's behalf. We do this here
                 // so it can benefit from a single near-startup prefs commit.
                 SessionInformation currentSession = new SessionInformation(now, realTime);
 
                 SharedPreferences prefs = GeckoApp.this.getSharedPreferences();
                 SharedPreferences.Editor editor = prefs.edit();
                 editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, false);
+
+                if (!mLastSessionCrashed) {
+                    // The last session terminated normally,
+                    // so we can reset the count of successive crashes.
+                    editor.putInt(GeckoApp.PREFS_CRASHED_COUNT, 0);
+                }
+
                 currentSession.recordBegin(editor);
                 editor.apply();
 
                 final HealthRecorder rec = mHealthRecorder;
                 if (rec != null) {
                     rec.setCurrentSession(currentSession);
                     rec.processDelayed();
                 } else {
@@ -2008,16 +2068,20 @@ public abstract class GeckoApp
             public void run() {
                 SharedPreferences prefs = GeckoApp.this.getSharedPreferences();
                 SharedPreferences.Editor editor = prefs.edit();
                 editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, true);
                 if (rec != null) {
                     rec.recordSessionEnd("P", editor);
                 }
 
+                // onPause might in fact be called even after a crash, but in that case the
+                // crash reporter will record this fact for us and we'll pick it up in onCreate.
+                mLastSessionCrashed = false;
+
                 // If we haven't done it before, cleanup any old files in our old temp dir
                 if (prefs.getBoolean(GeckoApp.PREFS_CLEANUP_TEMP_FILES, true)) {
                     File tempDir = GeckoLoader.getGREDir(GeckoApp.this);
                     FileUtils.delTree(tempDir, new FileUtils.NameAndAgeFilter(null, ONE_DAY_MS), false);
 
                     editor.putBoolean(GeckoApp.PREFS_CLEANUP_TEMP_FILES, false);
                 }
 
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
@@ -161,16 +161,18 @@ OnSharedPreferenceChangeListener
 
     private static final String ACTION_STUMBLER_UPLOAD_PREF = AppConstants.ANDROID_PACKAGE_NAME + ".STUMBLER_PREF";
 
 
     // This isn't a Gecko pref, even if it looks like one.
     private static final String PREFS_BROWSER_LOCALE = "locale";
 
     public static final String PREFS_RESTORE_SESSION = NON_PREF_PREFIX + "restoreSession3";
+    public static final String PREFS_RESTORE_SESSION_FROM_CRASH = "browser.sessionstore.resume_from_crash";
+    public static final String PREFS_RESTORE_SESSION_MAX_CRASH_RESUMES = "browser.sessionstore.max_resumed_crashes";
     public static final String PREFS_TAB_QUEUE = NON_PREF_PREFIX + "tab_queue";
     public static final String PREFS_TAB_QUEUE_LAST_SITE = NON_PREF_PREFIX + "last_site";
     public static final String PREFS_TAB_QUEUE_LAST_TIME = NON_PREF_PREFIX + "last_time";
 
     private static final String PREFS_DYNAMIC_TOOLBAR = "browser.chrome.dynamictoolbar";
 
     // These values are chosen to be distinct from other Activity constants.
     private static final int REQUEST_CODE_PREF_SCREEN = 5;
--- a/mobile/android/components/SessionStore.js
+++ b/mobile/android/components/SessionStore.js
@@ -12,16 +12,17 @@ Cu.import("resource://gre/modules/Servic
 
 XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Messaging", "resource://gre/modules/Messaging.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FormData", "resource://gre/modules/FormData.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch", "resource://gre/modules/TelemetryStopwatch.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Log", "resource://gre/modules/AndroidLog.jsm", "AndroidLog");
+XPCOMUtils.defineLazyModuleGetter(this, "SharedPreferences", "resource://gre/modules/SharedPreferences.jsm");
 
 function dump(a) {
   Services.console.logStringMessage(a);
 }
 
 let loggingEnabled = false;
 
 function log(a) {
@@ -37,16 +38,19 @@ function log(a) {
 
 const STATE_STOPPED = 0;
 const STATE_RUNNING = 1;
 
 const PRIVACY_NONE = 0;
 const PRIVACY_ENCRYPTED = 1;
 const PRIVACY_FULL = 2;
 
+const PREFS_RESTORE_FROM_CRASH = "browser.sessionstore.resume_from_crash";
+const PREFS_MAX_CRASH_RESUMES = "browser.sessionstore.max_resumed_crashes";
+
 function SessionStore() { }
 
 SessionStore.prototype = {
   classID: Components.ID("{8c1f07d6-cba3-4226-a315-8bd43d67d032}"),
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsISessionStore,
                                          Ci.nsIDOMEventListener,
                                          Ci.nsIObserver,
@@ -73,16 +77,27 @@ SessionStore.prototype = {
     this._sessionFileBackup = this._sessionFile.clone();
     this._sessionFile.append("sessionstore.js");
     this._sessionFileBackup.append("sessionstore.bak");
 
     this._loadState = STATE_STOPPED;
 
     this._interval = Services.prefs.getIntPref("browser.sessionstore.interval");
     this._maxTabsUndo = Services.prefs.getIntPref("browser.sessionstore.max_tabs_undo");
+
+    // Copy changes in Gecko settings to their Java counterparts,
+    // so the startup code can access them
+    Services.prefs.addObserver(PREFS_RESTORE_FROM_CRASH, function() {
+      SharedPreferences.forApp().setBoolPref(PREFS_RESTORE_FROM_CRASH,
+        Services.prefs.getBoolPref(PREFS_RESTORE_FROM_CRASH));
+    }, false);
+    Services.prefs.addObserver(PREFS_MAX_CRASH_RESUMES, function() {
+      SharedPreferences.forApp().setIntPref(PREFS_MAX_CRASH_RESUMES,
+        Services.prefs.getIntPref(PREFS_MAX_CRASH_RESUMES));
+    }, false);
   },
 
   _clearDisk: function ss_clearDisk() {
     OS.File.remove(this._sessionFile.path);
     OS.File.remove(this._sessionFileBackup.path);
   },
 
   observe: function ss_observe(aSubject, aTopic, aData) {