Browse Source

feat: add webContents.forcefullyCrashRenderer() to forcefully terminate a renderer process (#25756)

* feat: add webContents.crashProcess() to forcefully terminate a renderer process

* chore: fix up docs and tests

Co-authored-by: Samuel Attard <[email protected]>
trop[bot] 4 years ago
parent
commit
f298d4d54f

+ 9 - 0
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") {

+ 28 - 0
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

+ 27 - 0
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/frame_host/frame_tree_node.h"             // nogncheck
 #include "content/browser/frame_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);
@@ -2920,6 +2945,8 @@ v8::Local<v8::ObjectTemplate> 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)

+ 1 - 0
shell/browser/api/electron_api_web_contents.h

@@ -228,6 +228,7 @@ class WebContents : public gin::Wrappable<WebContents>,
   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);

+ 48 - 0
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()', () => {