From a189dc779e3badf4d7551a3e7af268dabf4e9761 Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Fri, 2 Oct 2020 14:50:24 -0700 Subject: [PATCH] feat: add webContents.forcefullyCrashRenderer() to forcefully terminate a renderer process (#25580) * feat: add webContents.forcefullyCrashRenderer() to forcefully terminate a renderer process * chore: fix up docs and tests --- chromium_src/BUILD.gn | 9 ++++ docs/api/web-contents.md | 28 +++++++++++ .../browser/api/electron_api_web_contents.cc | 27 +++++++++++ shell/browser/api/electron_api_web_contents.h | 1 + spec-main/api-web-contents-spec.ts | 48 +++++++++++++++++++ 5 files changed, 113 insertions(+) diff --git a/chromium_src/BUILD.gn b/chromium_src/BUILD.gn index b0d1dac617b0..3a38a8c490c4 100644 --- a/chromium_src/BUILD.gn +++ b/chromium_src/BUILD.gn @@ -266,6 +266,15 @@ static_library("chrome") { ] } } + + sources += [ "//chrome/browser/hang_monitor/hang_crash_dump.h" ] + if (is_mac) { + sources += [ "//chrome/browser/hang_monitor/hang_crash_dump_mac.cc" ] + } else if (is_win) { + sources += [ "//chrome/browser/hang_monitor/hang_crash_dump_win.cc" ] + } else { + sources += [ "//chrome/browser/hang_monitor/hang_crash_dump.cc" ] + } } source_set("plugins") { diff --git a/docs/api/web-contents.md b/docs/api/web-contents.md index c24fe4af894f..842182e3db4e 100644 --- a/docs/api/web-contents.md +++ b/docs/api/web-contents.md @@ -998,6 +998,34 @@ Navigates to the specified offset from the "current entry". Returns `Boolean` - Whether the renderer process has crashed. +#### `contents.forcefullyCrashRenderer()` + +Forcefully terminates the renderer process that is currently hosting this +`webContents`. This will cause the `render-process-gone` event to be emitted +with the `reason=killed || reason=crashed`. Please note that some webContents share renderer +processes and therefore calling this method may also crash the host process +for other webContents as well. + +Calling `reload()` immediately after calling this +method will force the reload to occur in a new process. This should be used +when this process is unstable or unusable, for instance in order to recover +from the `unresponsive` event. + +```js +contents.on('unresponsive', async () => { + const { response } = await dialog.showMessageBox({ + message: 'App X has become unresponsive', + title: 'Do you want to try forcefully reloading the app?', + buttons: ['OK', 'Cancel'], + cancelId: 1 + }) + if (response === 0) { + contents.forcefullyCrashRenderer() + contents.reload() + } +}) +``` + #### `contents.setUserAgent(userAgent)` * `userAgent` String diff --git a/shell/browser/api/electron_api_web_contents.cc b/shell/browser/api/electron_api_web_contents.cc index eb9adc9ebdb2..a71c2d8ea11f 100644 --- a/shell/browser/api/electron_api_web_contents.cc +++ b/shell/browser/api/electron_api_web_contents.cc @@ -21,6 +21,7 @@ #include "base/threading/thread_task_runner_handle.h" #include "base/values.h" #include "chrome/browser/browser_process.h" +#include "chrome/browser/hang_monitor/hang_crash_dump.h" #include "chrome/browser/ssl/security_state_tab_helper.h" #include "content/browser/renderer_host/frame_tree_node.h" // nogncheck #include "content/browser/renderer_host/render_frame_host_manager.h" // nogncheck @@ -1763,6 +1764,30 @@ bool WebContents::IsCrashed() const { return web_contents()->IsCrashed(); } +void WebContents::ForcefullyCrashRenderer() { + content::RenderWidgetHostView* view = + web_contents()->GetRenderWidgetHostView(); + if (!view) + return; + + content::RenderWidgetHost* rwh = view->GetRenderWidgetHost(); + if (!rwh) + return; + + content::RenderProcessHost* rph = rwh->GetProcess(); + if (rph) { +#if defined(OS_LINUX) || defined(OS_CHROMEOS) + // A generic |CrashDumpHungChildProcess()| is not implemented for Linux. + // Instead we send an explicit IPC to crash on the renderer's IO thread. + rph->ForceCrash(); +#else + // Try to generate a crash report for the hung process. + CrashDumpHungChildProcess(rph->GetProcess().Handle()); + rph->Shutdown(content::RESULT_CODE_HUNG); +#endif + } +} + void WebContents::SetUserAgent(const std::string& user_agent) { web_contents()->SetUserAgentOverride( blink::UserAgentOverride::UserAgentOnly(user_agent), false); @@ -2921,6 +2946,8 @@ v8::Local WebContents::FillObjectTemplate( .SetMethod("_goForward", &WebContents::GoForward) .SetMethod("_goToOffset", &WebContents::GoToOffset) .SetMethod("isCrashed", &WebContents::IsCrashed) + .SetMethod("forcefullyCrashRenderer", + &WebContents::ForcefullyCrashRenderer) .SetMethod("setUserAgent", &WebContents::SetUserAgent) .SetMethod("getUserAgent", &WebContents::GetUserAgent) .SetMethod("savePage", &WebContents::SavePage) diff --git a/shell/browser/api/electron_api_web_contents.h b/shell/browser/api/electron_api_web_contents.h index 05f8677f2e5e..0c1387d137d6 100644 --- a/shell/browser/api/electron_api_web_contents.h +++ b/shell/browser/api/electron_api_web_contents.h @@ -228,6 +228,7 @@ class WebContents : public gin::Wrappable, const std::string GetWebRTCIPHandlingPolicy() const; void SetWebRTCIPHandlingPolicy(const std::string& webrtc_ip_handling_policy); bool IsCrashed() const; + void ForcefullyCrashRenderer(); void SetUserAgent(const std::string& user_agent); std::string GetUserAgent(); void InsertCSS(const std::string& css); diff --git a/spec-main/api-web-contents-spec.ts b/spec-main/api-web-contents-spec.ts index 9040496cca09..536c13734f40 100644 --- a/spec-main/api-web-contents-spec.ts +++ b/spec-main/api-web-contents-spec.ts @@ -1245,6 +1245,54 @@ describe('webContents module', () => { }); }); + const crashPrefs = [ + { + nodeIntegration: true + }, + { + sandbox: true + } + ]; + + const nicePrefs = (o: any) => { + let s = ''; + for (const key of Object.keys(o)) { + s += `${key}=${o[key]}, `; + } + return `(${s.slice(0, s.length - 2)})`; + }; + + for (const prefs of crashPrefs) { + describe(`crash with webPreferences ${nicePrefs(prefs)}`, () => { + let w: BrowserWindow; + beforeEach(async () => { + w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }); + await w.loadURL('about:blank'); + }); + afterEach(closeAllWindows); + + it('isCrashed() is false by default', () => { + expect(w.webContents.isCrashed()).to.equal(false); + }); + + it('forcefullyCrashRenderer() crashes the process with reason=killed||crashed', async () => { + expect(w.webContents.isCrashed()).to.equal(false); + const crashEvent = emittedOnce(w.webContents, 'render-process-gone'); + w.webContents.forcefullyCrashRenderer(); + const [, details] = await crashEvent; + expect(details.reason === 'killed' || details.reason === 'crashed').to.equal(true, 'reason should be killed || crashed'); + expect(w.webContents.isCrashed()).to.equal(true); + }); + + it('a crashed process is recoverable with reload()', async () => { + expect(w.webContents.isCrashed()).to.equal(false); + w.webContents.forcefullyCrashRenderer(); + w.webContents.reload(); + expect(w.webContents.isCrashed()).to.equal(false); + }); + }); + } + // Destroying webContents in its event listener is going to crash when // Electron is built in Debug mode. describe('destroy()', () => {