Bug 340345 - 'DOM timeouts can fire during a sync XMLHttpRequest'. r+sr=jst, a=blocking1.9.1+
authorBen Turner <bent.mozilla@gmail.com>
Tue, 17 Feb 2009 12:07:39 -0800
changeset 25074 5d6362d85fb480edf37b0fee3e66fa60b34cca19
parent 25073 902fc255d4e89cae3423c7d21a133c1813bb0bb2
child 25075 d3e09494d40371f9f866201563d5b103dd42acf1
push idunknown
push userunknown
push dateunknown
reviewersblocking1.9.1
bugs340345
milestone1.9.2a1pre
Bug 340345 - 'DOM timeouts can fire during a sync XMLHttpRequest'. r+sr=jst, a=blocking1.9.1+
content/base/src/nsXMLHttpRequest.cpp
dom/public/base/nsPIDOMWindow.h
dom/src/base/nsGlobalWindow.cpp
dom/src/base/nsGlobalWindow.h
--- a/content/base/src/nsXMLHttpRequest.cpp
+++ b/content/base/src/nsXMLHttpRequest.cpp
@@ -496,16 +496,40 @@ nsACProxyListener::OnChannelRedirect(nsI
 
 NS_IMETHODIMP
 nsACProxyListener::GetInterface(const nsIID & aIID, void **aResult)
 {
   return QueryInterface(aIID, aResult);
 }
 
 /**
+ * Simple class to resume timeouts on a window asynchronously.
+ */
+class nsResumeTimeoutsRunnable : public nsIRunnable
+{
+public:
+  NS_DECL_ISUPPORTS
+
+  nsResumeTimeoutsRunnable(nsPIDOMWindow* aWindow)
+  : mWindow(aWindow)
+  {
+    NS_ASSERTION(aWindow, "Null pointer!");
+  }
+
+  NS_IMETHOD Run() {
+    return mWindow->ResumeTimeouts();
+  }
+
+private:
+  nsCOMPtr<nsPIDOMWindow> mWindow;
+};
+
+NS_IMPL_THREADSAFE_ISUPPORTS1(nsResumeTimeoutsRunnable, nsIRunnable)
+
+/**
  * Gets the nsIDocument given the script context. Will return nsnull on failure.
  *
  * @param aScriptContext the script context to get the document for; can be null
  *
  * @return the document associated with the script context
  */
 static already_AddRefed<nsIDocument>
 GetDocumentFromScriptContext(nsIScriptContext *aScriptContext)
@@ -2758,23 +2782,40 @@ nsXMLHttpRequest::Send(nsIVariant *aBody
   // that this needs to come after the AsyncOpen() and rv check, because this
   // can run script that would try to restart this request, and that could end
   // up doing our AsyncOpen on a null channel if the reentered AsyncOpen fails.
   ChangeState(XML_HTTP_REQUEST_SENT);
 
   // If we're synchronous, spin an event loop here and wait
   if (!(mState & XML_HTTP_REQUEST_ASYNC)) {
     mState |= XML_HTTP_REQUEST_SYNCLOOPING;
+
+    nsCOMPtr<nsIRunnable> resumeTimeoutRunnable;
+    if (mOwner) {
+      nsCOMPtr<nsIDOMWindow> topWindow;
+      if (NS_SUCCEEDED(mOwner->GetTop(getter_AddRefs(topWindow)))) {
+        nsCOMPtr<nsPIDOMWindow> suspendedWindow(do_QueryInterface(topWindow));
+        if (suspendedWindow) {
+          suspendedWindow->SuspendTimeouts();
+          resumeTimeoutRunnable = new nsResumeTimeoutsRunnable(suspendedWindow);
+        }
+      }
+    }
+
     nsIThread *thread = NS_GetCurrentThread();
     while (mState & XML_HTTP_REQUEST_SYNCLOOPING) {
       if (!NS_ProcessNextEvent(thread)) {
         rv = NS_ERROR_UNEXPECTED;
         break;
       }
     }
+
+    if (resumeTimeoutRunnable) {
+      NS_DispatchToCurrentThread(resumeTimeoutRunnable);
+    }
   } else {
     if (!mUploadComplete &&
         HasListenersFor(NS_LITERAL_STRING(UPLOADPROGRESS_STR)) ||
         (mUpload && mUpload->HasListenersFor(NS_LITERAL_STRING(PROGRESS_STR)))) {
       StartProgressEventTimer();
     }
     DispatchProgressEvent(this, NS_LITERAL_STRING(LOADSTART_STR), PR_FALSE,
                           0, 0);
--- a/dom/public/base/nsPIDOMWindow.h
+++ b/dom/public/base/nsPIDOMWindow.h
@@ -266,16 +266,19 @@ public:
 
   // Returns an object containing the window's state.  This also suspends
   // all running timeouts in the window.
   virtual nsresult SaveWindowState(nsISupports **aState) = 0;
 
   // Restore the window state from aState.
   virtual nsresult RestoreWindowState(nsISupports *aState) = 0;
 
+  // Suspend timeouts in this window and in child windows.
+  virtual void SuspendTimeouts() = 0;
+
   // Resume suspended timeouts in this window and in child windows.
   virtual nsresult ResumeTimeouts() = 0;
   
   // Fire any DOM notification events related to things that happened while
   // the window was frozen.
   virtual nsresult FireDelayedDOMEvents() = 0;
 
   virtual PRBool IsFrozen() const = 0;
--- a/dom/src/base/nsGlobalWindow.cpp
+++ b/dom/src/base/nsGlobalWindow.cpp
@@ -618,17 +618,18 @@ nsGlobalWindow::nsGlobalWindow(nsGlobalW
     mBlockScriptedClosingFlag(PR_FALSE),
     mFireOfflineStatusChangeEventOnThaw(PR_FALSE),
     mCreatingInnerWindow(PR_FALSE),
     mIsChrome(PR_FALSE),
     mTimeoutInsertionPoint(nsnull),
     mTimeoutPublicIdCounter(1),
     mTimeoutFiringDepth(0),
     mJSObject(nsnull),
-    mPendingStorageEvents(nsnull)
+    mPendingStorageEvents(nsnull),
+    mTimeoutsSuspendDepth(0)
 #ifdef DEBUG
     , mSetOpenerWindowCalled(PR_FALSE)
 #endif
 {
   memset(mScriptGlobals, 0, sizeof(mScriptGlobals));
   nsLayoutStatics::AddRef();
 
   // Initialize the PRCList (this).
@@ -7433,17 +7434,17 @@ nsGlobalWindow::SetTimeoutOrInterval(nsI
   if (subsumes) {
     timeout->mPrincipal = subjectPrincipal;
   } else {
     timeout->mPrincipal = ourPrincipal;
   }
 
   PRTime delta = (PRTime)realInterval * PR_USEC_PER_MSEC;
 
-  if (!IsFrozen()) {
+  if (!IsFrozen() && !mTimeoutsSuspendDepth) {
     // If we're not currently frozen, then we set timeout->mWhen to be the
     // actual firing time of the timer (i.e., now + delta). We also actually
     // create a timer and fire it off.
 
     timeout->mWhen = PR_Now() + delta;
 
     timeout->mTimer = do_CreateInstance("@mozilla.org/timer;1", &rv);
     if (NS_FAILED(rv)) {
@@ -7546,17 +7547,17 @@ nsGlobalWindow::SetTimeoutOrInterval(PRB
 }
 
 // static
 void
 nsGlobalWindow::RunTimeout(nsTimeout *aTimeout)
 {
   // If a modal dialog is open for this window, return early. Pending
   // timeouts will run when the modal dialog is dismissed.
-  if (IsInModalState()) {
+  if (IsInModalState() || mTimeoutsSuspendDepth) {
     return;
   }
 
   NS_ASSERTION(IsInnerWindow(), "Timeout running on outer window!");
   NS_ASSERTION(!IsFrozen(), "Timeout running on a window in the bfcache!");
 
   nsTimeout *nextTimeout, *timeout;
   nsTimeout *last_expired_timeout, *last_insertion_point;
@@ -8363,16 +8364,20 @@ nsGlobalWindow::RestoreWindowState(nsISu
   return NS_OK;
 }
 
 void
 nsGlobalWindow::SuspendTimeouts()
 {
   FORWARD_TO_INNER_VOID(SuspendTimeouts, ());
 
+  if (++mTimeoutsSuspendDepth != 1) {
+    return;
+  }
+
   nsDOMThreadService* dts = nsDOMThreadService::get();
   if (dts) {
     dts->SuspendWorkersForGlobal(static_cast<nsIScriptGlobalObject*>(this));
   }
 
   PRTime now = PR_Now();
   for (nsTimeout *t = FirstTimeout(); IsTimeout(t); t = t->Next()) {
     // Change mWhen to be the time remaining for this timer.    
@@ -8423,16 +8428,21 @@ nsGlobalWindow::SuspendTimeouts()
   }
 }
 
 nsresult
 nsGlobalWindow::ResumeTimeouts()
 {
   FORWARD_TO_INNER(ResumeTimeouts, (), NS_ERROR_NOT_INITIALIZED);
 
+  NS_ASSERTION(mTimeoutsSuspendDepth, "Mismatched calls to ResumeTimeouts!");
+  if (--mTimeoutsSuspendDepth != 0) {
+    return NS_OK;
+  }
+
   nsDOMThreadService* dts = nsDOMThreadService::get();
   if (dts) {
     dts->ResumeWorkersForGlobal(static_cast<nsIScriptGlobalObject*>(this));
   }
 
   // Restore all of the timeouts, using the stored time remaining
   // (stored in timeout->mWhen).
 
--- a/dom/src/base/nsGlobalWindow.h
+++ b/dom/src/base/nsGlobalWindow.h
@@ -298,16 +298,17 @@ public:
   virtual NS_HIDDEN_(nsIPrincipal*) GetOpenerScriptPrincipal();
 
   virtual NS_HIDDEN_(PopupControlState) PushPopupControlState(PopupControlState state, PRBool aForce) const;
   virtual NS_HIDDEN_(void) PopPopupControlState(PopupControlState state) const;
   virtual NS_HIDDEN_(PopupControlState) GetPopupControlState() const;
 
   virtual NS_HIDDEN_(nsresult) SaveWindowState(nsISupports **aState);
   virtual NS_HIDDEN_(nsresult) RestoreWindowState(nsISupports *aState);
+  virtual NS_HIDDEN_(void) SuspendTimeouts();
   virtual NS_HIDDEN_(nsresult) ResumeTimeouts();
   virtual NS_HIDDEN_(nsresult) FireDelayedDOMEvents();
   virtual NS_HIDDEN_(PRBool) IsFrozen() const
   {
     return mIsFrozen;
   }
 
   virtual NS_HIDDEN_(PRBool) WouldReuseInnerWindow(nsIDocument *aNewDocument);
@@ -594,18 +595,16 @@ protected:
 
   // If aLookForCallerOnJSStack is true, this method will look at the JS stack
   // to determine who the caller is.  If it's false, it'll use |this| as the
   // caller.
   PRBool WindowExists(const nsAString& aName, PRBool aLookForCallerOnJSStack);
 
   already_AddRefed<nsIWidget> GetMainWidget();
 
-  void SuspendTimeouts();
-
   void Freeze()
   {
     NS_ASSERTION(!IsFrozen(), "Double-freezing?");
     mIsFrozen = PR_TRUE;
   }
 
   void Thaw()
   {
@@ -728,16 +727,18 @@ protected:
 
   // These member variables are used on both inner and the outer windows.
   nsCOMPtr<nsIPrincipal> mDocumentPrincipal;
   nsCOMPtr<nsIDocument> mDoc;  // For fast access to principals
   JSObject* mJSObject;
 
   nsDataHashtable<nsStringHashKey, PRBool> *mPendingStorageEvents;
 
+  PRUint32 mTimeoutsSuspendDepth;
+
 #ifdef DEBUG
   PRBool mSetOpenerWindowCalled;
   PRUint32 mSerial;
   nsCOMPtr<nsIURI> mLastOpenedURI;
 #endif
 
   nsCOMPtr<nsIDOMOfflineResourceList> mApplicationCache;