From e1d9df224c631e8828035149dee39e55f82ca40c Mon Sep 17 00:00:00 2001 From: "trop[bot]" <37223003+trop[bot]@users.noreply.github.com> Date: Tue, 3 Dec 2024 09:55:43 -0800 Subject: [PATCH] feat: WebFrameMain.collectJavaScriptCallStack() (#44937) * feat: WebFrameMain.unresponsiveDocumentJSCallStack Co-authored-by: Samuel Maddock * Revert "feat: WebFrameMain.unresponsiveDocumentJSCallStack" This reverts commit e0612bc1a00a5282cba5df97da3c9c90e96ef244. Co-authored-by: Samuel Maddock * feat: frame.collectJavaScriptCallStack() Co-authored-by: Samuel Maddock * feat: frame.collectJavaScriptCallStack() Co-authored-by: Samuel Maddock * Update web-frame-main.md Co-authored-by: Sam Maddock --------- Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com> Co-authored-by: Samuel Maddock --- docs/api/web-frame-main.md | 23 ++++++++ .../api/electron_api_web_frame_main.cc | 59 +++++++++++++++++++ .../browser/api/electron_api_web_frame_main.h | 11 ++++ spec/api-web-frame-main-spec.ts | 33 ++++++++++- spec/index.js | 6 ++ 5 files changed, 131 insertions(+), 1 deletion(-) diff --git a/docs/api/web-frame-main.md b/docs/api/web-frame-main.md index 87d35051f081..45b01d5b25a2 100644 --- a/docs/api/web-frame-main.md +++ b/docs/api/web-frame-main.md @@ -142,6 +142,29 @@ ipcRenderer.on('port', (e, msg) => { }) ``` +#### `frame.collectJavaScriptCallStack()` _Experimental_ + +Returns `Promise | Promise` - A promise that resolves with the currently running JavaScript call +stack. If no JavaScript runs in the frame, the promise will never resolve. In cases where the call stack is +otherwise unable to be collected, it will return `undefined`. + +This can be useful to determine why the frame is unresponsive in cases where there's long-running JavaScript. +For more information, see the [proposed Crash Reporting API.](https://wicg.github.io/crash-reporting/) + +```js +const { app } = require('electron') + +app.commandLine.appendSwitch('enable-features', 'DocumentPolicyIncludeJSCallStacksInCrashReports') + +app.on('web-contents-created', (_, webContents) => { + webContents.on('unresponsive', async () => { + // Interrupt execution and collect call stack from unresponsive renderer + const callStack = await webContents.mainFrame.collectJavaScriptCallStack() + console.log('Renderer unresponsive\n', callStack) + }) +}) +``` + ### Instance Properties #### `frame.ipc` _Readonly_ diff --git a/shell/browser/api/electron_api_web_frame_main.cc b/shell/browser/api/electron_api_web_frame_main.cc index 6415e3019f98..7ced5e5763cd 100644 --- a/shell/browser/api/electron_api_web_frame_main.cc +++ b/shell/browser/api/electron_api_web_frame_main.cc @@ -9,9 +9,11 @@ #include #include +#include "base/feature_list.h" #include "base/logging.h" #include "base/no_destructor.h" #include "content/browser/renderer_host/render_frame_host_impl.h" // nogncheck +#include "content/browser/renderer_host/render_process_host_impl.h" // nogncheck #include "content/public/browser/frame_tree_node_id.h" #include "content/public/browser/render_frame_host.h" #include "content/public/common/isolated_world_ids.h" @@ -429,6 +431,61 @@ std::vector WebFrameMain::FramesInSubtree() const { return frame_hosts; } +v8::Local WebFrameMain::CollectDocumentJSCallStack( + gin::Arguments* args) { + gin_helper::Promise promise(args->isolate()); + v8::Local handle = promise.GetHandle(); + + if (render_frame_disposed_) { + promise.RejectWithErrorMessage( + "Render frame was disposed before WebFrameMain could be accessed"); + return handle; + } + + if (!base::FeatureList::IsEnabled( + blink::features::kDocumentPolicyIncludeJSCallStacksInCrashReports)) { + promise.RejectWithErrorMessage( + "DocumentPolicyIncludeJSCallStacksInCrashReports is not enabled"); + return handle; + } + + content::RenderProcessHostImpl* rph_impl = + static_cast(render_frame_->GetProcess()); + + rph_impl->GetJavaScriptCallStackGeneratorInterface() + ->CollectJavaScriptCallStack( + base::BindOnce(&WebFrameMain::CollectedJavaScriptCallStack, + weak_factory_.GetWeakPtr(), std::move(promise))); + + return handle; +} + +void WebFrameMain::CollectedJavaScriptCallStack( + gin_helper::Promise promise, + const std::string& untrusted_javascript_call_stack, + const std::optional& remote_frame_token) { + if (render_frame_disposed_) { + promise.RejectWithErrorMessage( + "Render frame was disposed before call stack was received"); + return; + } + + const blink::LocalFrameToken& frame_token = render_frame_->GetFrameToken(); + if (remote_frame_token == frame_token) { + base::Value base_value(untrusted_javascript_call_stack); + promise.Resolve(base_value); + } else if (!remote_frame_token) { + // Failed to collect call stack. See logic in: + // third_party/blink/renderer/controller/javascript_call_stack_collector.cc + promise.Resolve(base::Value()); + } else { + // Requests for call stacks can be initiated on an old RenderProcessHost + // then be received after a frame swap. + LOG(ERROR) << "Received call stack from old RPH"; + promise.Resolve(base::Value()); + } +} + void WebFrameMain::DOMContentLoaded() { Emit("dom-ready"); } @@ -461,6 +518,8 @@ void WebFrameMain::FillObjectTemplate(v8::Isolate* isolate, v8::Local templ) { gin_helper::ObjectTemplateBuilder(isolate, templ) .SetMethod("executeJavaScript", &WebFrameMain::ExecuteJavaScript) + .SetMethod("collectJavaScriptCallStack", + &WebFrameMain::CollectDocumentJSCallStack) .SetMethod("reload", &WebFrameMain::Reload) .SetMethod("isDestroyed", &WebFrameMain::IsDestroyed) .SetMethod("_send", &WebFrameMain::Send) diff --git a/shell/browser/api/electron_api_web_frame_main.h b/shell/browser/api/electron_api_web_frame_main.h index 5f765dca48a6..07d50afb8cf9 100644 --- a/shell/browser/api/electron_api_web_frame_main.h +++ b/shell/browser/api/electron_api_web_frame_main.h @@ -36,6 +36,11 @@ template class Handle; } // namespace gin +namespace gin_helper { +template +class Promise; +} // namespace gin_helper + namespace electron::api { class WebContents; @@ -128,6 +133,12 @@ class WebFrameMain final : public gin::Wrappable, std::vector Frames() const; std::vector FramesInSubtree() const; + v8::Local CollectDocumentJSCallStack(gin::Arguments* args); + void CollectedJavaScriptCallStack( + gin_helper::Promise promise, + const std::string& untrusted_javascript_call_stack, + const std::optional& remote_frame_token); + void DOMContentLoaded(); mojo::Remote renderer_api_; diff --git a/spec/api-web-frame-main-spec.ts b/spec/api-web-frame-main-spec.ts index 9d1c9edae76a..6a1a5873db84 100644 --- a/spec/api-web-frame-main-spec.ts +++ b/spec/api-web-frame-main-spec.ts @@ -21,8 +21,16 @@ describe('webFrameMain module', () => { type Server = { server: http.Server, url: string, crossOriginUrl: string } /** Creates an HTTP server whose handler embeds the given iframe src. */ - const createServer = async (): Promise => { + const createServer = async (options: { + headers?: Record + } = {}): Promise => { const server = http.createServer((req, res) => { + if (options.headers) { + for (const [k, v] of Object.entries(options.headers)) { + res.setHeader(k, v); + } + } + const params = new URLSearchParams(new URL(req.url || '', `http://${req.headers.host}`).search || ''); if (params.has('frameSrc')) { res.end(``); @@ -444,6 +452,29 @@ describe('webFrameMain module', () => { }); }); + describe('webFrameMain.collectJavaScriptCallStack', () => { + let server: Server; + before(async () => { + server = await createServer({ + headers: { + 'Document-Policy': 'include-js-call-stacks-in-crash-reports' + } + }); + }); + after(() => { + server.server.close(); + }); + + it('collects call stack during JS execution', async () => { + const w = new BrowserWindow({ show: false }); + await w.loadURL(server.url); + const callStackPromise = w.webContents.mainFrame.collectJavaScriptCallStack(); + w.webContents.mainFrame.executeJavaScript('"run a lil js"'); + const callStack = await callStackPromise; + expect(callStack).to.be.a('string'); + }); + }); + describe('"frame-created" event', () => { it('emits when the main frame is created', async () => { const w = new BrowserWindow({ show: false }); diff --git a/spec/index.js b/spec/index.js index 344b00615682..985d0b43ae75 100644 --- a/spec/index.js +++ b/spec/index.js @@ -33,6 +33,12 @@ app.commandLine.appendSwitch('host-resolver-rules', [ 'MAP notfound.localhost2 ~NOTFOUND' ].join(', ')); +// Enable features required by tests. +app.commandLine.appendSwitch('enable-features', [ + // spec/api-web-frame-main-spec.ts + 'DocumentPolicyIncludeJSCallStacksInCrashReports' +].join(',')); + global.standardScheme = 'app'; global.zoomScheme = 'zoom'; global.serviceWorkerScheme = 'sw';