Browse Source

Merge pull request #9113 from electron/window_close_patch

browser: make destruction of webContents async
Cheng Zhao 8 years ago
parent
commit
9e0c308b09

+ 1 - 1
atom/browser/api/atom_api_browser_view.cc

@@ -75,7 +75,7 @@ void BrowserView::Init(v8::Isolate* isolate,
 }
 
 BrowserView::~BrowserView() {
-  api_web_contents_->DestroyWebContents();
+  api_web_contents_->DestroyWebContents(true /* async */);
 }
 
 // static

+ 19 - 8
atom/browser/api/atom_api_web_contents.cc

@@ -417,15 +417,28 @@ WebContents::~WebContents() {
       guest_delegate_->Destroy();
 
     RenderViewDeleted(web_contents()->GetRenderViewHost());
-    DestroyWebContents();
+
+    if (type_ == WEB_VIEW) {
+      DestroyWebContents(false /* async */);
+    } else {
+      if (type_ == BROWSER_WINDOW && owner_window()) {
+        owner_window()->CloseContents(nullptr);
+      } else {
+        DestroyWebContents(true /* async */);
+      }
+      // The WebContentsDestroyed will not be called automatically because we
+      // destroy the webContents in the next tick. So we have to manually
+      // call it here to make sure "destroyed" event is emitted.
+      WebContentsDestroyed();
+    }
   }
 }
 
-void WebContents::DestroyWebContents() {
+void WebContents::DestroyWebContents(bool async) {
   // This event is only for internal use, which is emitted when WebContents is
   // being destroyed.
   Emit("will-destroy");
-  ResetManagedWebContents();
+  ResetManagedWebContents(async);
 }
 
 bool WebContents::DidAddMessageToConsole(content::WebContents* source,
@@ -477,7 +490,7 @@ void WebContents::AddNewContents(content::WebContents* source,
   if (Emit("-add-new-contents", api_web_contents, disposition, user_gesture,
       initial_rect.x(), initial_rect.y(), initial_rect.width(),
       initial_rect.height())) {
-    api_web_contents->DestroyWebContents();
+    api_web_contents->DestroyWebContents(true /* async */);
   }
 }
 
@@ -816,10 +829,8 @@ void WebContents::DidFinishNavigation(
 
 void WebContents::TitleWasSet(content::NavigationEntry* entry,
                               bool explicit_set) {
-  if (entry)
-    Emit("-page-title-updated", entry->GetTitle(), explicit_set);
-  else
-    Emit("-page-title-updated", "", explicit_set);
+  auto title = entry ? entry->GetTitle() : base::string16();
+  Emit("page-title-updated", title, explicit_set);
 }
 
 void WebContents::DidUpdateFaviconURL(

+ 1 - 1
atom/browser/api/atom_api_web_contents.h

@@ -78,7 +78,7 @@ class WebContents : public mate::TrackableObject<WebContents>,
                              v8::Local<v8::FunctionTemplate> prototype);
 
   // Notifies to destroy any guest web contents before destroying self.
-  void DestroyWebContents();
+  void DestroyWebContents(bool async);
 
   int64_t GetID() const;
   int GetProcessID() const;

+ 1 - 1
atom/browser/api/atom_api_window.cc

@@ -173,7 +173,7 @@ void Window::WillDestroyNativeObject() {
 }
 
 void Window::OnWindowClosed() {
-  api_web_contents_->DestroyWebContents();
+  api_web_contents_->DestroyWebContents(true /* async */);
 
   RemoveFromWeakMap();
   window_->RemoveObserver(this);

+ 7 - 2
atom/browser/common_web_contents_delegate.cc

@@ -188,8 +188,13 @@ void CommonWebContentsDelegate::SetOwnerWindow(
   }
 }
 
-void CommonWebContentsDelegate::ResetManagedWebContents() {
-  web_contents_.reset();
+void CommonWebContentsDelegate::ResetManagedWebContents(bool async) {
+  if (async) {
+    base::ThreadTaskRunnerHandle::Get()->DeleteSoon(FROM_HERE,
+                                                    web_contents_.release());
+  } else {
+    web_contents_.reset();
+  }
 }
 
 content::WebContents* CommonWebContentsDelegate::GetWebContents() const {

+ 1 - 1
atom/browser/common_web_contents_delegate.h

@@ -112,7 +112,7 @@ class CommonWebContentsDelegate
 #endif
 
   // Destroy the managed InspectableWebContents object.
-  void ResetManagedWebContents();
+  void ResetManagedWebContents(bool async);
 
  private:
   // Callback for when DevToolsSaveToFile has completed.

+ 1 - 5
lib/browser/api/browser-window.js

@@ -76,13 +76,9 @@ BrowserWindow.prototype._init = function () {
 
   // Change window title to page title.
   this.webContents.on('page-title-updated', (event, title) => {
-    // The page-title-updated event is not emitted immediately (see #3645), so
-    // when the callback is called the BrowserWindow might have been closed.
-    if (this.isDestroyed()) return
-
     // Route the event to BrowserWindow.
     this.emit('page-title-updated', event, title)
-    if (!event.defaultPrevented) this.setTitle(title)
+    if (!this.isDestroyed() && !event.defaultPrevented) this.setTitle(title)
   })
 
   // Sometimes the webContents doesn't get focus when window is shown, so we

+ 0 - 7
lib/browser/api/web-contents.js

@@ -268,13 +268,6 @@ WebContents.prototype._init = function () {
     this.reload()
   })
 
-  // Delays the page-title-updated event to next tick.
-  this.on('-page-title-updated', function (...args) {
-    setImmediate(() => {
-      this.emit('page-title-updated', ...args)
-    })
-  })
-
   app.emit('web-contents-created', {}, this)
 }
 

+ 68 - 0
spec/api-browser-window-spec.js

@@ -86,6 +86,42 @@ describe('BrowserWindow module', function () {
   })
 
   describe('BrowserWindow.close()', function () {
+    let server
+
+    before(function (done) {
+      server = http.createServer((request, response) => {
+        switch (request.url) {
+          case '/404':
+            response.statusCode = '404'
+            response.end()
+            break
+          case '/301':
+            response.statusCode = '301'
+            response.setHeader('Location', '/200')
+            response.end()
+            break
+          case '/200':
+            response.statusCode = '200'
+            response.end('hello')
+            break
+          case '/title':
+            response.statusCode = '200'
+            response.end('<title>Hello</title>')
+            break
+          default:
+            done('unsupported endpoint')
+        }
+      }).listen(0, '127.0.0.1', () => {
+        server.url = 'http://127.0.0.1:' + server.address().port
+        done()
+      })
+    })
+
+    after(function () {
+      server.close()
+      server = null
+    })
+
     it('should emit unload handler', function (done) {
       w.webContents.on('did-finish-load', function () {
         w.close()
@@ -109,6 +145,38 @@ describe('BrowserWindow module', function () {
       })
       w.loadURL('file://' + path.join(fixtures, 'api', 'beforeunload-false.html'))
     })
+
+    it('should not crash when invoked synchronously inside navigation observer', function (done) {
+      const events = [
+        { name: 'did-start-loading', url: `${server.url}/200` },
+        { name: 'did-get-redirect-request', url: `${server.url}/301` },
+        { name: 'did-get-response-details', url: `${server.url}/200` },
+        { name: 'dom-ready', url: `${server.url}/200` },
+        { name: 'page-title-updated', url: `${server.url}/title` },
+        { name: 'did-stop-loading', url: `${server.url}/200` },
+        { name: 'did-finish-load', url: `${server.url}/200` },
+        { name: 'did-frame-finish-load', url: `${server.url}/200` },
+        { name: 'did-fail-load', url: `${server.url}/404` }
+      ]
+      const responseEvent = 'window-webContents-destroyed'
+
+      function* genNavigationEvent () {
+        let eventOptions = null
+        while ((eventOptions = events.shift()) && events.length) {
+          let w = new BrowserWindow({show: false})
+          eventOptions.id = w.id
+          eventOptions.responseEvent = responseEvent
+          ipcRenderer.send('test-webcontents-navigation-observer', eventOptions)
+          yield 1
+        }
+      }
+
+      let gen = genNavigationEvent()
+      ipcRenderer.on(responseEvent, function () {
+        if (!gen.next().value) done()
+      })
+      gen.next()
+    })
   })
 
   describe('window.close()', function () {

+ 66 - 0
spec/api-web-contents-spec.js

@@ -542,4 +542,70 @@ describe('webContents module', function () {
       })
     })
   })
+
+  describe('destroy()', () => {
+    let server
+
+    before(function (done) {
+      server = http.createServer((request, response) => {
+        switch (request.url) {
+          case '/404':
+            response.statusCode = '404'
+            response.end()
+            break
+          case '/301':
+            response.statusCode = '301'
+            response.setHeader('Location', '/200')
+            response.end()
+            break
+          case '/200':
+            response.statusCode = '200'
+            response.end('hello')
+            break
+          default:
+            done('unsupported endpoint')
+        }
+      }).listen(0, '127.0.0.1', () => {
+        server.url = 'http://127.0.0.1:' + server.address().port
+        done()
+      })
+    })
+
+    after(function () {
+      server.close()
+      server = null
+    })
+
+    it('should not crash when invoked synchronously inside navigation observer', (done) => {
+      const events = [
+        { name: 'did-start-loading', url: `${server.url}/200` },
+        { name: 'did-get-redirect-request', url: `${server.url}/301` },
+        { name: 'did-get-response-details', url: `${server.url}/200` },
+        { name: 'dom-ready', url: `${server.url}/200` },
+        { name: 'did-stop-loading', url: `${server.url}/200` },
+        { name: 'did-finish-load', url: `${server.url}/200` },
+        // FIXME: Multiple Emit calls inside an observer assume that object
+        // will be alive till end of the observer. Synchronous `destroy` api
+        // violates this contract and crashes.
+        // { name: 'did-frame-finish-load', url: `${server.url}/200` },
+        { name: 'did-fail-load', url: `${server.url}/404` }
+      ]
+      const responseEvent = 'webcontents-destroyed'
+
+      function* genNavigationEvent () {
+        let eventOptions = null
+        while ((eventOptions = events.shift()) && events.length) {
+          eventOptions.responseEvent = responseEvent
+          ipcRenderer.send('test-webcontents-navigation-observer', eventOptions)
+          yield 1
+        }
+      }
+
+      let gen = genNavigationEvent()
+      ipcRenderer.on(responseEvent, () => {
+        if (!gen.next().value) done()
+      })
+      gen.next()
+    })
+  })
 })

+ 21 - 0
spec/static/main.js

@@ -338,6 +338,27 @@ ipcMain.on('crash-service-pid', (event, pid) => {
   event.returnValue = null
 })
 
+ipcMain.on('test-webcontents-navigation-observer', (event, options) => {
+  let contents = null
+  let destroy = () => {}
+  if (options.id) {
+    const w = BrowserWindow.fromId(options.id)
+    contents = w.webContents
+    destroy = () => w.close()
+  } else {
+    contents = webContents.create()
+    destroy = () => contents.destroy()
+  }
+
+  contents.once(options.name, () => destroy())
+
+  contents.once('destroyed', () => {
+    event.sender.send(options.responseEvent)
+  })
+
+  contents.loadURL(options.url)
+})
+
 // Suspend listeners until the next event and then restore them
 const suspendListeners = (emitter, eventName, callback) => {
   const listeners = emitter.listeners(eventName)