feat: add WebFrameMain detached property (#43473)

* feat: add WebFrameMain detached property

fix: throw instead of returning null senderFrame

test: detached frames

fix: ensure IPCs of pending deletion RFHs are dispatched

fix: lookup WFM by FTN ID to dispatch IPCs

feat: add frame.isDestroyed()

return null

fix: return undefined

docs: add null to all frame properties

refactor: option c, return null and emit warning

refactor: add routingId & processId to navigation events

test: null frame property

docs: clarify warning message

better wording

clarify null frame

fix: browserwindow spec

* maybe fix 🤷

* fix: use updated util #43722

* docs: add notice for frame change of behavior

* docs: clarify why frame properties may be null

* lint

* wip

* fix: content::FrameTreeNodeId lookup and converter

* refactor: avoid holey array deoptimization

---------

Co-authored-by: John Kleinschmidt <jkleinsc@electronjs.org>
This commit is contained in:
Sam Maddock 2024-10-11 18:33:53 -04:00 committed by GitHub
parent 527efc01a4
commit 8b3d70a2a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 410 additions and 126 deletions

View file

@ -33,6 +33,37 @@
#include "shell/common/node_includes.h"
#include "shell/common/v8_value_serializer.h"
namespace {
// RenderFrameHost (RFH) exists as a child of a FrameTreeNode. When a
// cross-origin navigation occurs, the FrameTreeNode swaps RFHs. After
// swapping, the old RFH will be marked for deletion and run any unload
// listeners. If an IPC is sent during an unload/beforeunload listener,
// it's possible that it arrives after the RFH swap and has been
// detached from the FrameTreeNode.
bool IsDetachedFrameHost(content::RenderFrameHost* rfh) {
if (!rfh)
return true;
// RenderFrameCreated is called for speculative frames which may not be
// used in certain cross-origin navigations. Invoking
// RenderFrameHost::GetLifecycleState currently crashes when called for
// speculative frames so we need to filter it out for now. Check
// https://crbug.com/1183639 for details on when this can be removed.
auto* rfh_impl = static_cast<content::RenderFrameHostImpl*>(rfh);
// During cross-origin navigation, a RFH may be swapped out of its
// FrameTreeNode with a new RFH. In these cases, it's marked for
// deletion. As this pending deletion RFH won't be following future
// swaps, we need to indicate that its been pinned.
return (rfh_impl->lifecycle_state() !=
content::RenderFrameHostImpl::LifecycleStateImpl::kSpeculative &&
rfh->GetLifecycleState() ==
content::RenderFrameHost::LifecycleState::kPendingDeletion);
}
} // namespace
namespace gin {
template <>
@ -57,45 +88,64 @@ struct Converter<blink::mojom::PageVisibilityState> {
namespace electron::api {
// FrameTreeNodeId -> WebFrameMain*
// Using FrameTreeNode allows us to track frame across navigations. This
// is most similar to how <iframe> works.
typedef std::unordered_map<content::FrameTreeNodeId,
WebFrameMain*,
content::FrameTreeNodeId::Hasher>
WebFrameMainIdMap;
FrameTreeNodeIdMap;
WebFrameMainIdMap& GetWebFrameMainMap() {
static base::NoDestructor<WebFrameMainIdMap> instance;
// Token -> WebFrameMain*
// Maps exact RFH to a WebFrameMain instance.
typedef std::map<content::GlobalRenderFrameHostToken, WebFrameMain*>
FrameTokenMap;
FrameTreeNodeIdMap& GetFrameTreeNodeIdMap() {
static base::NoDestructor<FrameTreeNodeIdMap> instance;
return *instance;
}
FrameTokenMap& GetFrameTokenMap() {
static base::NoDestructor<FrameTokenMap> instance;
return *instance;
}
// static
WebFrameMain* WebFrameMain::FromFrameTreeNodeId(
content::FrameTreeNodeId frame_tree_node_id) {
WebFrameMainIdMap& frame_map = GetWebFrameMainMap();
// Pinned frames aren't tracked across navigations so only non-pinned
// frames will be retrieved.
FrameTreeNodeIdMap& frame_map = GetFrameTreeNodeIdMap();
auto iter = frame_map.find(frame_tree_node_id);
auto* web_frame = iter == frame_map.end() ? nullptr : iter->second;
return web_frame;
}
// static
WebFrameMain* WebFrameMain::FromFrameToken(
content::GlobalRenderFrameHostToken frame_token) {
FrameTokenMap& frame_map = GetFrameTokenMap();
auto iter = frame_map.find(frame_token);
auto* web_frame = iter == frame_map.end() ? nullptr : iter->second;
return web_frame;
}
// static
WebFrameMain* WebFrameMain::FromRenderFrameHost(content::RenderFrameHost* rfh) {
if (!rfh)
return nullptr;
// TODO(codebytere): remove after refactoring away from FrameTreeNodeId as map
// key.
auto* ftn =
static_cast<content::RenderFrameHostImpl*>(rfh)->frame_tree_node();
if (!ftn)
return nullptr;
return FromFrameTreeNodeId(rfh->GetFrameTreeNodeId());
return FromFrameToken(rfh->GetGlobalFrameToken());
}
gin::WrapperInfo WebFrameMain::kWrapperInfo = {gin::kEmbedderNativeGin};
WebFrameMain::WebFrameMain(content::RenderFrameHost* rfh)
: frame_tree_node_id_(rfh->GetFrameTreeNodeId()), render_frame_(rfh) {
GetWebFrameMainMap().emplace(frame_tree_node_id_, this);
: frame_tree_node_id_(rfh->GetFrameTreeNodeId()),
frame_token_(rfh->GetGlobalFrameToken()),
render_frame_(rfh),
render_frame_detached_(IsDetachedFrameHost(rfh)) {
GetFrameTreeNodeIdMap().emplace(frame_tree_node_id_, this);
GetFrameTokenMap().emplace(frame_token_, this);
}
WebFrameMain::~WebFrameMain() {
@ -104,18 +154,24 @@ WebFrameMain::~WebFrameMain() {
void WebFrameMain::Destroyed() {
MarkRenderFrameDisposed();
GetWebFrameMainMap().erase(frame_tree_node_id_);
GetFrameTreeNodeIdMap().erase(frame_tree_node_id_);
GetFrameTokenMap().erase(frame_token_);
Unpin();
}
void WebFrameMain::MarkRenderFrameDisposed() {
render_frame_ = nullptr;
render_frame_detached_ = true;
render_frame_disposed_ = true;
TeardownMojoConnection();
}
// Should only be called when swapping frames.
void WebFrameMain::UpdateRenderFrameHost(content::RenderFrameHost* rfh) {
// Should only be called when swapping frames.
GetFrameTokenMap().erase(frame_token_);
frame_token_ = rfh->GetGlobalFrameToken();
GetFrameTokenMap().emplace(frame_token_, this);
render_frame_disposed_ = false;
render_frame_ = rfh;
TeardownMojoConnection();
@ -186,6 +242,10 @@ bool WebFrameMain::Reload() {
return render_frame_->Reload();
}
bool WebFrameMain::IsDestroyed() const {
return render_frame_disposed_;
}
void WebFrameMain::Send(v8::Isolate* isolate,
bool internal,
const std::string& channel,
@ -275,8 +335,12 @@ void WebFrameMain::PostMessage(v8::Isolate* isolate,
std::move(transferable_message));
}
int WebFrameMain::FrameTreeNodeIDAsInt() const {
return frame_tree_node_id_.value();
bool WebFrameMain::Detached() const {
return render_frame_detached_;
}
content::FrameTreeNodeId WebFrameMain::FrameTreeNodeID() const {
return frame_tree_node_id_;
}
std::string WebFrameMain::Name() const {
@ -389,29 +453,17 @@ gin::Handle<WebFrameMain> WebFrameMain::From(v8::Isolate* isolate,
return handle;
}
// static
gin::Handle<WebFrameMain> WebFrameMain::FromOrNull(
v8::Isolate* isolate,
content::RenderFrameHost* rfh) {
if (!rfh)
return gin::Handle<WebFrameMain>();
auto* web_frame = FromRenderFrameHost(rfh);
if (!web_frame)
return gin::Handle<WebFrameMain>();
return gin::CreateHandle(isolate, web_frame);
}
// static
void WebFrameMain::FillObjectTemplate(v8::Isolate* isolate,
v8::Local<v8::ObjectTemplate> templ) {
gin_helper::ObjectTemplateBuilder(isolate, templ)
.SetMethod("executeJavaScript", &WebFrameMain::ExecuteJavaScript)
.SetMethod("reload", &WebFrameMain::Reload)
.SetMethod("isDestroyed", &WebFrameMain::IsDestroyed)
.SetMethod("_send", &WebFrameMain::Send)
.SetMethod("_postMessage", &WebFrameMain::PostMessage)
.SetProperty("frameTreeNodeId", &WebFrameMain::FrameTreeNodeIDAsInt)
.SetProperty("detached", &WebFrameMain::Detached)
.SetProperty("frameTreeNodeId", &WebFrameMain::FrameTreeNodeID)
.SetProperty("name", &WebFrameMain::Name)
.SetProperty("osProcessId", &WebFrameMain::OSProcessID)
.SetProperty("processId", &WebFrameMain::ProcessID)
@ -446,22 +498,38 @@ v8::Local<v8::Value> FromID(gin_helper::ErrorThrower thrower,
auto* rfh =
content::RenderFrameHost::FromID(render_process_id, render_frame_id);
if (!rfh)
return v8::Undefined(thrower.isolate());
return WebFrameMain::From(thrower.isolate(), rfh).ToV8();
}
v8::Local<v8::Value> FromIDOrNull(gin_helper::ErrorThrower thrower,
int render_process_id,
int render_frame_id) {
v8::Local<v8::Value> FromIdIfExists(gin_helper::ErrorThrower thrower,
int render_process_id,
int render_frame_id) {
if (!electron::Browser::Get()->is_ready()) {
thrower.ThrowError("WebFrameMain is available only after app ready");
return v8::Null(thrower.isolate());
}
auto* rfh =
content::RenderFrameHost* rfh =
content::RenderFrameHost::FromID(render_process_id, render_frame_id);
WebFrameMain* web_frame = WebFrameMain::FromRenderFrameHost(rfh);
if (!web_frame)
return v8::Null(thrower.isolate());
return gin::CreateHandle(thrower.isolate(), web_frame).ToV8();
}
return WebFrameMain::FromOrNull(thrower.isolate(), rfh).ToV8();
v8::Local<v8::Value> FromFtnIdIfExists(gin_helper::ErrorThrower thrower,
int frame_tree_node_id) {
if (!electron::Browser::Get()->is_ready()) {
thrower.ThrowError("WebFrameMain is available only after app ready");
return v8::Null(thrower.isolate());
}
WebFrameMain* web_frame = WebFrameMain::FromFrameTreeNodeId(
content::FrameTreeNodeId(frame_tree_node_id));
if (!web_frame)
return v8::Null(thrower.isolate());
return gin::CreateHandle(thrower.isolate(), web_frame).ToV8();
}
void Initialize(v8::Local<v8::Object> exports,
@ -472,7 +540,8 @@ void Initialize(v8::Local<v8::Object> exports,
gin_helper::Dictionary dict(isolate, exports);
dict.Set("WebFrameMain", WebFrameMain::GetConstructor(context));
dict.SetMethod("fromId", &FromID);
dict.SetMethod("fromIdOrNull", &FromIDOrNull);
dict.SetMethod("_fromIdIfExists", &FromIdIfExists);
dict.SetMethod("_fromFtnIdIfExists", &FromFtnIdIfExists);
}
} // namespace