fix: destroy WebContents synchronously on shutdown (#15541)
This commit is contained in:
		
					parent
					
						
							
								6162d9090d
							
						
					
				
			
			
				commit
				
					
						746beb0d8b
					
				
			
		
					 12 changed files with 122 additions and 13 deletions
				
			
		| 
						 | 
					@ -66,6 +66,7 @@ void BrowserView::Init(v8::Isolate* isolate,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  web_contents_.Reset(isolate, web_contents.ToV8());
 | 
					  web_contents_.Reset(isolate, web_contents.ToV8());
 | 
				
			||||||
  api_web_contents_ = web_contents.get();
 | 
					  api_web_contents_ = web_contents.get();
 | 
				
			||||||
 | 
					  Observe(web_contents->web_contents());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  view_.reset(
 | 
					  view_.reset(
 | 
				
			||||||
      NativeBrowserView::Create(api_web_contents_->managed_web_contents()));
 | 
					      NativeBrowserView::Create(api_web_contents_->managed_web_contents()));
 | 
				
			||||||
| 
						 | 
					@ -74,7 +75,16 @@ void BrowserView::Init(v8::Isolate* isolate,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
BrowserView::~BrowserView() {
 | 
					BrowserView::~BrowserView() {
 | 
				
			||||||
  api_web_contents_->DestroyWebContents(true /* async */);
 | 
					  if (api_web_contents_) {  // destroy() is called
 | 
				
			||||||
 | 
					    // Destroy WebContents asynchronously unless app is shutting down,
 | 
				
			||||||
 | 
					    // because destroy() might be called inside WebContents's event handler.
 | 
				
			||||||
 | 
					    api_web_contents_->DestroyWebContents(!Browser::Get()->is_shutting_down());
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void BrowserView::WebContentsDestroyed() {
 | 
				
			||||||
 | 
					  api_web_contents_ = nullptr;
 | 
				
			||||||
 | 
					  web_contents_.Reset();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// static
 | 
					// static
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,6 +10,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include "atom/browser/api/trackable_object.h"
 | 
					#include "atom/browser/api/trackable_object.h"
 | 
				
			||||||
#include "atom/browser/native_browser_view.h"
 | 
					#include "atom/browser/native_browser_view.h"
 | 
				
			||||||
 | 
					#include "content/public/browser/web_contents_observer.h"
 | 
				
			||||||
#include "native_mate/handle.h"
 | 
					#include "native_mate/handle.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace gfx {
 | 
					namespace gfx {
 | 
				
			||||||
| 
						 | 
					@ -29,7 +30,8 @@ namespace api {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class WebContents;
 | 
					class WebContents;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BrowserView : public mate::TrackableObject<BrowserView> {
 | 
					class BrowserView : public mate::TrackableObject<BrowserView>,
 | 
				
			||||||
 | 
					                    public content::WebContentsObserver {
 | 
				
			||||||
 public:
 | 
					 public:
 | 
				
			||||||
  static mate::WrappableBase* New(mate::Arguments* args);
 | 
					  static mate::WrappableBase* New(mate::Arguments* args);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -47,6 +49,9 @@ class BrowserView : public mate::TrackableObject<BrowserView> {
 | 
				
			||||||
              const mate::Dictionary& options);
 | 
					              const mate::Dictionary& options);
 | 
				
			||||||
  ~BrowserView() override;
 | 
					  ~BrowserView() override;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // content::WebContentsObserver:
 | 
				
			||||||
 | 
					  void WebContentsDestroyed() override;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 private:
 | 
					 private:
 | 
				
			||||||
  void Init(v8::Isolate* isolate,
 | 
					  void Init(v8::Isolate* isolate,
 | 
				
			||||||
            v8::Local<v8::Object> wrapper,
 | 
					            v8::Local<v8::Object> wrapper,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -381,7 +381,9 @@ void BrowserWindow::Cleanup() {
 | 
				
			||||||
  if (host)
 | 
					  if (host)
 | 
				
			||||||
    host->GetWidget()->RemoveInputEventObserver(this);
 | 
					    host->GetWidget()->RemoveInputEventObserver(this);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  api_web_contents_->DestroyWebContents(true /* async */);
 | 
					  // Destroy WebContents asynchronously unless app is shutting down,
 | 
				
			||||||
 | 
					  // because destroy() might be called inside WebContents's event handler.
 | 
				
			||||||
 | 
					  api_web_contents_->DestroyWebContents(!Browser::Get()->is_shutting_down());
 | 
				
			||||||
  Observe(nullptr);
 | 
					  Observe(nullptr);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,6 +17,7 @@
 | 
				
			||||||
#include "atom/browser/atom_browser_main_parts.h"
 | 
					#include "atom/browser/atom_browser_main_parts.h"
 | 
				
			||||||
#include "atom/browser/atom_javascript_dialog_manager.h"
 | 
					#include "atom/browser/atom_javascript_dialog_manager.h"
 | 
				
			||||||
#include "atom/browser/atom_navigation_throttle.h"
 | 
					#include "atom/browser/atom_navigation_throttle.h"
 | 
				
			||||||
 | 
					#include "atom/browser/browser.h"
 | 
				
			||||||
#include "atom/browser/child_web_contents_tracker.h"
 | 
					#include "atom/browser/child_web_contents_tracker.h"
 | 
				
			||||||
#include "atom/browser/lib/bluetooth_chooser.h"
 | 
					#include "atom/browser/lib/bluetooth_chooser.h"
 | 
				
			||||||
#include "atom/browser/native_window.h"
 | 
					#include "atom/browser/native_window.h"
 | 
				
			||||||
| 
						 | 
					@ -493,14 +494,25 @@ WebContents::~WebContents() {
 | 
				
			||||||
    RenderViewDeleted(web_contents()->GetRenderViewHost());
 | 
					    RenderViewDeleted(web_contents()->GetRenderViewHost());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (type_ == WEB_VIEW) {
 | 
					    if (type_ == WEB_VIEW) {
 | 
				
			||||||
 | 
					      // For webview simply destroy the WebContents immediately.
 | 
				
			||||||
 | 
					      // TODO(zcbenz): Add an internal API for webview instead of using
 | 
				
			||||||
 | 
					      // destroy(), so we don't have to add a special branch here.
 | 
				
			||||||
      DestroyWebContents(false /* async */);
 | 
					      DestroyWebContents(false /* async */);
 | 
				
			||||||
    } else {
 | 
					    } else if (type_ == BROWSER_WINDOW && owner_window()) {
 | 
				
			||||||
      if (type_ == BROWSER_WINDOW && owner_window()) {
 | 
					      // For BrowserWindow we should close the window and clean up everything
 | 
				
			||||||
 | 
					      // before WebContents is destroyed.
 | 
				
			||||||
      for (ExtendedWebContentsObserver& observer : observers_)
 | 
					      for (ExtendedWebContentsObserver& observer : observers_)
 | 
				
			||||||
        observer.OnCloseContents();
 | 
					        observer.OnCloseContents();
 | 
				
			||||||
 | 
					      // BrowserWindow destroys WebContents asynchronously, manually emit the
 | 
				
			||||||
 | 
					      // destroyed event here.
 | 
				
			||||||
 | 
					      WebContentsDestroyed();
 | 
				
			||||||
 | 
					    } else if (Browser::Get()->is_shutting_down()) {
 | 
				
			||||||
 | 
					      // Destroy WebContents directly when app is shutting down.
 | 
				
			||||||
 | 
					      DestroyWebContents(false /* async */);
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
 | 
					      // Destroy WebContents asynchronously unless app is shutting down,
 | 
				
			||||||
 | 
					      // because destroy() might be called inside WebContents's event handler.
 | 
				
			||||||
      DestroyWebContents(true /* async */);
 | 
					      DestroyWebContents(true /* async */);
 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      // The WebContentsDestroyed will not be called automatically because we
 | 
					      // The WebContentsDestroyed will not be called automatically because we
 | 
				
			||||||
      // destroy the webContents in the next tick. So we have to manually
 | 
					      // destroy the webContents in the next tick. So we have to manually
 | 
				
			||||||
      // call it here to make sure "destroyed" event is emitted.
 | 
					      // call it here to make sure "destroyed" event is emitted.
 | 
				
			||||||
| 
						 | 
					@ -567,6 +579,7 @@ void WebContents::AddNewContents(
 | 
				
			||||||
  if (Emit("-add-new-contents", api_web_contents, disposition, user_gesture,
 | 
					  if (Emit("-add-new-contents", api_web_contents, disposition, user_gesture,
 | 
				
			||||||
           initial_rect.x(), initial_rect.y(), initial_rect.width(),
 | 
					           initial_rect.x(), initial_rect.y(), initial_rect.width(),
 | 
				
			||||||
           initial_rect.height(), tracker->url, tracker->frame_name)) {
 | 
					           initial_rect.height(), tracker->url, tracker->frame_name)) {
 | 
				
			||||||
 | 
					    // TODO(zcbenz): Can we make this sync?
 | 
				
			||||||
    api_web_contents->DestroyWebContents(true /* async */);
 | 
					    api_web_contents->DestroyWebContents(true /* async */);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -106,7 +106,19 @@ class WebContents : public mate::TrackableObject<WebContents>,
 | 
				
			||||||
  static void BuildPrototype(v8::Isolate* isolate,
 | 
					  static void BuildPrototype(v8::Isolate* isolate,
 | 
				
			||||||
                             v8::Local<v8::FunctionTemplate> prototype);
 | 
					                             v8::Local<v8::FunctionTemplate> prototype);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Notifies to destroy any guest web contents before destroying self.
 | 
					  // Destroy the managed content::WebContents instance.
 | 
				
			||||||
 | 
					  //
 | 
				
			||||||
 | 
					  // Note: The |async| should only be |true| when users are expecting to use the
 | 
				
			||||||
 | 
					  // webContents immediately after the call. Always pass |false| if you are not
 | 
				
			||||||
 | 
					  // sure.
 | 
				
			||||||
 | 
					  // See https://github.com/electron/electron/issues/8930.
 | 
				
			||||||
 | 
					  //
 | 
				
			||||||
 | 
					  // Note: When destroying a webContents member inside a destructor, the |async|
 | 
				
			||||||
 | 
					  // should always be |false|, otherwise the destroy task might be delayed after
 | 
				
			||||||
 | 
					  // normal shutdown procedure, resulting in an assertion.
 | 
				
			||||||
 | 
					  // The normal pattern for calling this method in destructor is:
 | 
				
			||||||
 | 
					  // api_web_contents_->DestroyWebContents(!Browser::Get()->is_shutting_down())
 | 
				
			||||||
 | 
					  // See https://github.com/electron/electron/issues/15133.
 | 
				
			||||||
  void DestroyWebContents(bool async);
 | 
					  void DestroyWebContents(bool async);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void SetBackgroundThrottling(bool allowed);
 | 
					  void SetBackgroundThrottling(bool allowed);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,6 +5,7 @@
 | 
				
			||||||
#include "atom/browser/api/atom_api_web_contents_view.h"
 | 
					#include "atom/browser/api/atom_api_web_contents_view.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include "atom/browser/api/atom_api_web_contents.h"
 | 
					#include "atom/browser/api/atom_api_web_contents.h"
 | 
				
			||||||
 | 
					#include "atom/browser/browser.h"
 | 
				
			||||||
#include "atom/browser/ui/inspectable_web_contents_view.h"
 | 
					#include "atom/browser/ui/inspectable_web_contents_view.h"
 | 
				
			||||||
#include "atom/common/api/constructor.h"
 | 
					#include "atom/common/api/constructor.h"
 | 
				
			||||||
#include "content/public/browser/web_contents_user_data.h"
 | 
					#include "content/public/browser/web_contents_user_data.h"
 | 
				
			||||||
| 
						 | 
					@ -64,8 +65,11 @@ WebContentsView::WebContentsView(v8::Isolate* isolate,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
WebContentsView::~WebContentsView() {
 | 
					WebContentsView::~WebContentsView() {
 | 
				
			||||||
  if (api_web_contents_)
 | 
					  if (api_web_contents_) {  // destroy() is called
 | 
				
			||||||
    api_web_contents_->DestroyWebContents(false /* async */);
 | 
					    // Destroy WebContents asynchronously unless app is shutting down,
 | 
				
			||||||
 | 
					    // because destroy() might be called inside WebContents's event handler.
 | 
				
			||||||
 | 
					    api_web_contents_->DestroyWebContents(!Browser::Get()->is_shutting_down());
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void WebContentsView::WebContentsDestroyed() {
 | 
					void WebContentsView::WebContentsDestroyed() {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,10 @@
 | 
				
			||||||
'use strict'
 | 
					'use strict'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const chai = require('chai')
 | 
					const chai = require('chai')
 | 
				
			||||||
 | 
					const ChildProcess = require('child_process')
 | 
				
			||||||
const dirtyChai = require('dirty-chai')
 | 
					const dirtyChai = require('dirty-chai')
 | 
				
			||||||
 | 
					const path = require('path')
 | 
				
			||||||
 | 
					const { emittedOnce } = require('./events-helpers')
 | 
				
			||||||
const { closeWindow } = require('./window-helpers')
 | 
					const { closeWindow } = require('./window-helpers')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const { remote } = require('electron')
 | 
					const { remote } = require('electron')
 | 
				
			||||||
| 
						 | 
					@ -172,4 +175,14 @@ describe('BrowserView module', () => {
 | 
				
			||||||
      expect(views[0].webContents.id).to.equal(view.webContents.id)
 | 
					      expect(views[0].webContents.id).to.equal(view.webContents.id)
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('new BrowserView()', () => {
 | 
				
			||||||
 | 
					    it('does not crash on exit', async () => {
 | 
				
			||||||
 | 
					      const appPath = path.join(__dirname, 'fixtures', 'api', 'leak-exit-browserview.js')
 | 
				
			||||||
 | 
					      const electronPath = remote.getGlobal('process').execPath
 | 
				
			||||||
 | 
					      const appProcess = ChildProcess.spawn(electronPath, [appPath])
 | 
				
			||||||
 | 
					      const [code] = await emittedOnce(appProcess, 'close')
 | 
				
			||||||
 | 
					      expect(code).to.equal(0)
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
'use strict'
 | 
					'use strict'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const assert = require('assert')
 | 
					const assert = require('assert')
 | 
				
			||||||
 | 
					const ChildProcess = require('child_process')
 | 
				
			||||||
const fs = require('fs')
 | 
					const fs = require('fs')
 | 
				
			||||||
const http = require('http')
 | 
					const http = require('http')
 | 
				
			||||||
const path = require('path')
 | 
					const path = require('path')
 | 
				
			||||||
| 
						 | 
					@ -697,6 +698,16 @@ describe('webContents module', () => {
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('create()', () => {
 | 
				
			||||||
 | 
					    it('does not crash on exit', async () => {
 | 
				
			||||||
 | 
					      const appPath = path.join(__dirname, 'fixtures', 'api', 'leak-exit-webcontents.js')
 | 
				
			||||||
 | 
					      const electronPath = remote.getGlobal('process').execPath
 | 
				
			||||||
 | 
					      const appProcess = ChildProcess.spawn(electronPath, [appPath])
 | 
				
			||||||
 | 
					      const [code] = await emittedOnce(appProcess, 'close')
 | 
				
			||||||
 | 
					      expect(code).to.equal(0)
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Destroying webContents in its event listener is going to crash when
 | 
					  // Destroying webContents in its event listener is going to crash when
 | 
				
			||||||
  // Electron is built in Debug mode.
 | 
					  // Electron is built in Debug mode.
 | 
				
			||||||
  xdescribe('destroy()', () => {
 | 
					  xdescribe('destroy()', () => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,18 @@
 | 
				
			||||||
'use strict'
 | 
					'use strict'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const assert = require('assert')
 | 
					const assert = require('assert')
 | 
				
			||||||
 | 
					const chai = require('chai')
 | 
				
			||||||
 | 
					const ChildProcess = require('child_process')
 | 
				
			||||||
 | 
					const dirtyChai = require('dirty-chai')
 | 
				
			||||||
 | 
					const path = require('path')
 | 
				
			||||||
 | 
					const { emittedOnce } = require('./events-helpers')
 | 
				
			||||||
const { closeWindow } = require('./window-helpers')
 | 
					const { closeWindow } = require('./window-helpers')
 | 
				
			||||||
const { webContents, TopLevelWindow, WebContentsView } = require('electron').remote
 | 
					
 | 
				
			||||||
 | 
					const { remote } = require('electron')
 | 
				
			||||||
 | 
					const { webContents, TopLevelWindow, WebContentsView } = remote
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { expect } = chai
 | 
				
			||||||
 | 
					chai.use(dirtyChai)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe('WebContentsView', () => {
 | 
					describe('WebContentsView', () => {
 | 
				
			||||||
  let w = null
 | 
					  let w = null
 | 
				
			||||||
| 
						 | 
					@ -22,4 +32,14 @@ describe('WebContentsView', () => {
 | 
				
			||||||
      w.setContentView(new WebContentsView(web))
 | 
					      w.setContentView(new WebContentsView(web))
 | 
				
			||||||
    }, /The WebContents has already been added to a View/)
 | 
					    }, /The WebContents has already been added to a View/)
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('new WebContentsView()', () => {
 | 
				
			||||||
 | 
					    it('does not crash on exit', async () => {
 | 
				
			||||||
 | 
					      const appPath = path.join(__dirname, 'fixtures', 'api', 'leak-exit-webcontentsview.js')
 | 
				
			||||||
 | 
					      const electronPath = remote.getGlobal('process').execPath
 | 
				
			||||||
 | 
					      const appProcess = ChildProcess.spawn(electronPath, [appPath])
 | 
				
			||||||
 | 
					      const [code] = await emittedOnce(appProcess, 'close')
 | 
				
			||||||
 | 
					      expect(code).to.equal(0)
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										6
									
								
								spec/fixtures/api/leak-exit-browserview.js
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								spec/fixtures/api/leak-exit-browserview.js
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,6 @@
 | 
				
			||||||
 | 
					const { BrowserView, app } = require('electron')
 | 
				
			||||||
 | 
					app.on('ready', function () {
 | 
				
			||||||
 | 
					  new BrowserView({})  // eslint-disable-line
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  process.nextTick(() => app.quit())
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
							
								
								
									
										6
									
								
								spec/fixtures/api/leak-exit-webcontents.js
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								spec/fixtures/api/leak-exit-webcontents.js
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,6 @@
 | 
				
			||||||
 | 
					const { app, webContents } = require('electron')
 | 
				
			||||||
 | 
					app.on('ready', function () {
 | 
				
			||||||
 | 
					  webContents.create({})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  process.nextTick(() => app.quit())
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
							
								
								
									
										7
									
								
								spec/fixtures/api/leak-exit-webcontentsview.js
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								spec/fixtures/api/leak-exit-webcontentsview.js
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,7 @@
 | 
				
			||||||
 | 
					const { WebContentsView, app, webContents } = require('electron')
 | 
				
			||||||
 | 
					app.on('ready', function () {
 | 
				
			||||||
 | 
					  const web = webContents.create({})
 | 
				
			||||||
 | 
					  new WebContentsView(web)  // eslint-disable-line
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  process.nextTick(() => app.quit())
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue