Bug 1330954 - fix leakiness of SHIELD system add-on and re-enable test, r?mythmon draft
authorGijs Kruitbosch <gijskruitbosch@gmail.com>
Fri, 13 Jan 2017 13:42:47 +0000
changeset 460662 ec1eca3e571580322e5de0e3394c26dd99b9e1a3
parent 460618 91f5293e9a89056565493ed5073c3842b0ee9fdc
child 542104 46c3deae127c9c500dbdc682d35a18a205a7aadf
push id41451
push usergijskruitbosch@gmail.com
push dateFri, 13 Jan 2017 14:56:38 +0000
reviewersmythmon
bugs1330954
milestone53.0a1
Bug 1330954 - fix leakiness of SHIELD system add-on and re-enable test, r?mythmon There are 3 issues this tackles: - we were keeping a reference to the notification we added (this.notice), which kept windows alive. - we had the CleanupManager keep a reference to our close method, which kept a reference to us, which kept a reference to the window. - the fact that we use timeouts to call this.close() in 2 places meant this.close would get called multiple times, which meant we errored out on later occasions, because various things had been nulled out. This tidies up the timeouts when cleanup is called to avoid re-entrancy/errors/leaks. MozReview-Commit-ID: EYvK7bQEh3X
browser/extensions/shield-recipe-client/lib/CleanupManager.jsm
browser/extensions/shield-recipe-client/lib/Heartbeat.jsm
browser/extensions/shield-recipe-client/test/browser.ini
--- a/browser/extensions/shield-recipe-client/lib/CleanupManager.jsm
+++ b/browser/extensions/shield-recipe-client/lib/CleanupManager.jsm
@@ -1,21 +1,29 @@
 /* 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/. */
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["CleanupManager"];
 
-const cleanupHandlers = [];
+const cleanupHandlers = new Set();
 
 this.CleanupManager = {
   addCleanupHandler(handler) {
-    cleanupHandlers.push(handler);
+    cleanupHandlers.add(handler);
+  },
+
+  removeCleanupHandler(handler) {
+    cleanupHandlers.delete(handler);
   },
 
   cleanup() {
     for (const handler of cleanupHandlers) {
-      handler();
+      try {
+        handler();
+      } catch (ex) {
+        Cu.reportError(ex);
+      }
     }
   },
 };
--- a/browser/extensions/shield-recipe-client/lib/Heartbeat.jsm
+++ b/browser/extensions/shield-recipe-client/lib/Heartbeat.jsm
@@ -95,16 +95,17 @@ this.Heartbeat = class {
     this.eventEmitter = eventEmitter;
     this.sandboxManager = sandboxManager;
     this.options = options;
     this.surveyResults = {};
     this.buttons = null;
 
     // so event handlers are consistent
     this.handleWindowClosed = this.handleWindowClosed.bind(this);
+    this.close = this.close.bind(this);
 
     if (this.options.engagementButtonLabel) {
       this.buttons = [{
         label: this.options.engagementButtonLabel,
         callback: () => {
           // Let the consumer know user engaged.
           this.maybeNotifyHeartbeat("Engaged");
 
@@ -203,17 +204,17 @@ this.Heartbeat = class {
 
     const surveyDuration = Preferences.get(PREF_SURVEY_DURATION, 300) * 1000;
     this.surveyEndTimer = setTimeout(() => {
       this.maybeNotifyHeartbeat("SurveyExpired");
       this.close();
     }, surveyDuration);
 
     this.sandboxManager.addHold("heartbeat");
-    CleanupManager.addCleanupHandler(() => this.close());
+    CleanupManager.addCleanupHandler(this.close);
   }
 
   maybeNotifyHeartbeat(name, data = {}) {
     if (this.pingSent) {
       log.warn("Heartbeat event recieved after Telemetry ping sent. name:", name, "data:", data);
       return;
     }
 
@@ -276,20 +277,17 @@ this.Heartbeat = class {
         addClientId: true,
         addEnvironment: true,
       });
 
       // only for testing
       this.eventEmitter.emit("TelemetrySent", Cu.cloneInto(payload, this.sandboxManager.sandbox));
 
       // Survey is complete, clear out the expiry timer & survey configuration
-      if (this.surveyEndTimer) {
-        clearTimeout(this.surveyEndTimer);
-        this.surveyEndTimer = null;
-      }
+      this.endTimerIfPresent("surveyEndTimer");
 
       this.pingSent = true;
       this.surveyResults = null;
     }
 
     if (cleanup) {
       this.cleanup();
     }
@@ -310,37 +308,48 @@ this.Heartbeat = class {
     if (this.options.postAnswerUrl) {
       for (const key in engagementParams) {
         this.options.postAnswerUrl.searchParams.append(key, engagementParams[key]);
       }
       // Open the engagement URL in a new tab.
       this.chromeWindow.gBrowser.selectedTab = this.chromeWindow.gBrowser.addTab(this.options.postAnswerUrl.toString());
     }
 
-    if (this.surveyEndTimer) {
-      clearTimeout(this.surveyEndTimer);
-      this.surveyEndTimer = null;
+    this.endTimerIfPresent("surveyEndTimer");
+
+    this.engagementCloseTimer = setTimeout(() => this.close(), NOTIFICATION_TIME);
+  }
+
+  endTimerIfPresent(timerName) {
+    if (this[timerName]) {
+      clearTimeout(this[timerName]);
+      this[timerName] = null;
     }
-
-    setTimeout(() => this.close(), NOTIFICATION_TIME);
   }
 
   handleWindowClosed() {
     this.maybeNotifyHeartbeat("WindowClosed");
   }
 
   close() {
     this.notificationBox.removeNotification(this.notice);
     this.cleanup();
   }
 
   cleanup() {
+    // Kill the timers which might call things after we've cleaned up:
+    this.endTimerIfPresent("surveyEndTimer");
+    this.endTimerIfPresent("engagementCloseTimer");
+
     this.sandboxManager.removeHold("heartbeat");
     // remove listeners
     this.chromeWindow.removeEventListener("SSWindowClosing", this.handleWindowClosed);
     // remove references for garbage collection
     this.chromeWindow = null;
     this.notificationBox = null;
     this.notification = null;
+    this.notice = null;
     this.eventEmitter = null;
     this.sandboxManager = null;
+    // Ensure we don't re-enter and release the CleanupManager's reference to us:
+    CleanupManager.removeCleanupHandler(this.close);
   }
 };
--- a/browser/extensions/shield-recipe-client/test/browser.ini
+++ b/browser/extensions/shield-recipe-client/test/browser.ini
@@ -1,9 +1,8 @@
 [browser_driver_uuids.js]
 [browser_env_expressions.js]
 [browser_EventEmitter.js]
 [browser_Storage.js]
 [browser_Heartbeat.js]
-skip-if = true # bug 1325409
 [browser_NormandyApi.js]
   support-files =
     test_server.sjs