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 коммит произвёл GitHub
Родитель 527efc01a4
Коммит 8b3d70a2a3
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
20 изменённых файлов: 410 добавлений и 126 удалений

Просмотреть файл

@ -268,7 +268,8 @@ Returns:
* `event` Event
* `details` Object
* `deviceList` [HIDDevice[]](structures/hid-device.md)
* `frame` [WebFrameMain](web-frame-main.md)
* `frame` [WebFrameMain](web-frame-main.md) | null - The frame initiating this event.
May be `null` if accessed after the frame has either navigated or been destroyed.
* `callback` Function
* `deviceId` string | null (optional)
@ -332,7 +333,8 @@ Returns:
* `event` Event
* `details` Object
* `device` [HIDDevice](structures/hid-device.md)
* `frame` [WebFrameMain](web-frame-main.md)
* `frame` [WebFrameMain](web-frame-main.md) | null - The frame initiating this event.
May be `null` if accessed after the frame has either navigated or been destroyed.
Emitted after `navigator.hid.requestDevice` has been called and
`select-hid-device` has fired if a new device becomes available before
@ -347,7 +349,8 @@ Returns:
* `event` Event
* `details` Object
* `device` [HIDDevice](structures/hid-device.md)
* `frame` [WebFrameMain](web-frame-main.md)
* `frame` [WebFrameMain](web-frame-main.md) | null - The frame initiating this event.
May be `null` if accessed after the frame has either navigated or been destroyed.
Emitted after `navigator.hid.requestDevice` has been called and
`select-hid-device` has fired if a device has been removed before the callback
@ -473,7 +476,8 @@ Returns:
* `event` Event
* `details` Object
* `port` [SerialPort](structures/serial-port.md)
* `frame` [WebFrameMain](web-frame-main.md)
* `frame` [WebFrameMain](web-frame-main.md) | null - The frame initiating this event.
May be `null` if accessed after the frame has either navigated or been destroyed.
* `origin` string - The origin that the device has been revoked from.
Emitted after `SerialPort.forget()` has been called. This event can be used
@ -517,7 +521,8 @@ Returns:
* `event` Event
* `details` Object
* `deviceList` [USBDevice[]](structures/usb-device.md)
* `frame` [WebFrameMain](web-frame-main.md)
* `frame` [WebFrameMain](web-frame-main.md) | null - The frame initiating this event.
May be `null` if accessed after the frame has either navigated or been destroyed.
* `callback` Function
* `deviceId` string (optional)
@ -957,7 +962,8 @@ session.fromPartition('some-partition').setPermissionCheckHandler((webContents,
* `handler` Function | null
* `request` Object
* `frame` [WebFrameMain](web-frame-main.md) - Frame that is requesting access to media.
* `frame` [WebFrameMain](web-frame-main.md) | null - Frame that is requesting access to media.
May be `null` if accessed after the frame has either navigated or been destroyed.
* `securityOrigin` String - Origin of the page making the request.
* `videoRequested` Boolean - true if the web content requested a video stream.
* `audioRequested` Boolean - true if the web content requested an audio stream.
@ -1158,7 +1164,8 @@ app.whenReady().then(() => {
pin displayed on the device.
* `providePin`
This prompt is requesting that a pin be provided for the device.
* `frame` [WebFrameMain](web-frame-main.md)
* `frame` [WebFrameMain](web-frame-main.md) | null - The frame initiating this handler.
May be `null` if accessed after the frame has either navigated or been destroyed.
* `pin` string (optional) - The pin value to verify if `pairingKind` is `confirmPin`.
* `callback` Function
* `response` Object

Просмотреть файл

@ -4,7 +4,7 @@
* `frameId` Integer - The ID of the renderer frame that sent this message
* `returnValue` any - Set this to the value to be returned in a synchronous message
* `sender` [WebContents](../web-contents.md) - Returns the `webContents` that sent the message
* `senderFrame` [WebFrameMain](../web-frame-main.md) _Readonly_ - The frame that sent this message
* `senderFrame` [WebFrameMain](../web-frame-main.md) | null _Readonly_ - The frame that sent this message. May be `null` if accessed after the frame has either navigated or been destroyed.
* `ports` [MessagePortMain](../message-port-main.md)[] - A list of MessagePorts that were transferred with this message
* `reply` Function - A function that will send an IPC message to the renderer frame that sent the original message that you are currently handling. You should use this method to "reply" to the sent message in order to guarantee the reply will go to the correct process and frame.
* `channel` string

Просмотреть файл

@ -3,4 +3,4 @@
* `processId` Integer - The internal ID of the renderer process that sent this message
* `frameId` Integer - The ID of the renderer frame that sent this message
* `sender` [WebContents](../web-contents.md) - Returns the `webContents` that sent the message
* `senderFrame` [WebFrameMain](../web-frame-main.md) _Readonly_ - The frame that sent this message
* `senderFrame` [WebFrameMain](../web-frame-main.md) | null _Readonly_ - The frame that sent this message. May be `null` if accessed after the frame has either navigated or been destroyed.

Просмотреть файл

@ -242,8 +242,9 @@ Returns:
* `isSameDocument` boolean - This event does not fire for same document navigations using window.history api and reference fragment navigations.
This property is always set to `false` for this event.
* `isMainFrame` boolean - True if the navigation is taking place in a main frame.
* `frame` WebFrameMain - The frame to be navigated.
* `initiator` WebFrameMain (optional) - The frame which initiated the
* `frame` WebFrameMain | null - The frame to be navigated.
May be `null` if accessed after the frame has either navigated or been destroyed.
* `initiator` WebFrameMain | null (optional) - The frame which initiated the
navigation, which can be a parent frame (e.g. via `window.open` with a
frame's name), or null if the navigation was not initiated by a frame. This
can also be null if the initiating frame was deleted before the event was
@ -275,8 +276,9 @@ Returns:
* `isSameDocument` boolean - This event does not fire for same document navigations using window.history api and reference fragment navigations.
This property is always set to `false` for this event.
* `isMainFrame` boolean - True if the navigation is taking place in a main frame.
* `frame` WebFrameMain - The frame to be navigated.
* `initiator` WebFrameMain (optional) - The frame which initiated the
* `frame` WebFrameMain | null - The frame to be navigated.
May be `null` if accessed after the frame has either navigated or been destroyed.
* `initiator` WebFrameMain | null (optional) - The frame which initiated the
navigation, which can be a parent frame (e.g. via `window.open` with a
frame's name), or null if the navigation was not initiated by a frame. This
can also be null if the initiating frame was deleted before the event was
@ -306,8 +308,9 @@ Returns:
document. Examples of same document navigations are reference fragment
navigations, pushState/replaceState, and same page history navigation.
* `isMainFrame` boolean - True if the navigation is taking place in a main frame.
* `frame` WebFrameMain - The frame to be navigated.
* `initiator` WebFrameMain (optional) - The frame which initiated the
* `frame` WebFrameMain | null - The frame to be navigated.
May be `null` if accessed after the frame has either navigated or been destroyed.
* `initiator` WebFrameMain | null (optional) - The frame which initiated the
navigation, which can be a parent frame (e.g. via `window.open` with a
frame's name), or null if the navigation was not initiated by a frame. This
can also be null if the initiating frame was deleted before the event was
@ -330,8 +333,9 @@ Returns:
document. Examples of same document navigations are reference fragment
navigations, pushState/replaceState, and same page history navigation.
* `isMainFrame` boolean - True if the navigation is taking place in a main frame.
* `frame` WebFrameMain - The frame to be navigated.
* `initiator` WebFrameMain (optional) - The frame which initiated the
* `frame` WebFrameMain | null - The frame to be navigated.
May be `null` if accessed after the frame has either navigated or been destroyed.
* `initiator` WebFrameMain | null (optional) - The frame which initiated the
navigation, which can be a parent frame (e.g. via `window.open` with a
frame's name), or null if the navigation was not initiated by a frame. This
can also be null if the initiating frame was deleted before the event was
@ -361,8 +365,9 @@ Returns:
document. Examples of same document navigations are reference fragment
navigations, pushState/replaceState, and same page history navigation.
* `isMainFrame` boolean - True if the navigation is taking place in a main frame.
* `frame` WebFrameMain - The frame to be navigated.
* `initiator` WebFrameMain (optional) - The frame which initiated the
* `frame` WebFrameMain | null - The frame to be navigated.
May be `null` if accessed after the frame has either navigated or been destroyed.
* `initiator` WebFrameMain | null (optional) - The frame which initiated the
navigation, which can be a parent frame (e.g. via `window.open` with a
frame's name), or null if the navigation was not initiated by a frame. This
can also be null if the initiating frame was deleted before the event was
@ -743,7 +748,8 @@ Returns:
* `params` Object
* `x` Integer - x coordinate.
* `y` Integer - y coordinate.
* `frame` WebFrameMain - Frame from which the context menu was invoked.
* `frame` WebFrameMain | null - Frame from which the context menu was invoked.
May be `null` if accessed after the frame has either navigated or been destroyed.
* `linkURL` string - URL of the link that encloses the node the context menu
was invoked on.
* `linkText` string - Text associated with the link. May be an empty
@ -1010,7 +1016,8 @@ Returns:
* `event` Event
* `details` Object
* `frame` WebFrameMain
* `frame` WebFrameMain | null - The created frame.
May be `null` if accessed after the frame has either navigated or been destroyed.
Emitted when the [mainFrame](web-contents.md#contentsmainframe-readonly), an `<iframe>`, or a nested `<iframe>` is loaded within the page.

Просмотреть файл

@ -97,6 +97,10 @@ this limitation.
Returns `boolean` - Whether the reload was initiated successfully. Only results in `false` when the frame has no history.
#### `frame.isDestroyed()`
Returns `boolean` - Whether the frame is destroyed.
#### `frame.send(channel, ...args)`
* `channel` string
@ -231,6 +235,13 @@ A `string` representing the [visibility state](https://developer.mozilla.org/en-
See also how the [Page Visibility API](browser-window.md#page-visibility) is affected by other Electron APIs.
#### `frame.detached` _Readonly_
A `Boolean` representing whether the frame is detached from the frame tree. If a frame is accessed
while the corresponding page is running any [unload][] listeners, it may become detached as the
newly navigated page replaced it in the frame tree.
[SCA]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
[`postMessage`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage
[`MessagePortMain`]: message-port-main.md
[unload]: https://developer.mozilla.org/en-US/docs/Web/API/Window/unload_event

Просмотреть файл

@ -51,7 +51,8 @@ The following methods are available on instances of `WebRequest`:
* `method` string
* `webContentsId` Integer (optional)
* `webContents` WebContents (optional)
* `frame` WebFrameMain (optional)
* `frame` WebFrameMain | null (optional) - Requesting frame.
May be `null` if accessed after the frame has either navigated or been destroyed.
* `resourceType` string - Can be `mainFrame`, `subFrame`, `stylesheet`, `script`, `image`, `font`, `object`, `xhr`, `ping`, `cspReport`, `media`, `webSocket` or `other`.
* `referrer` string
* `timestamp` Double
@ -94,7 +95,8 @@ Some examples of valid `urls`:
* `method` string
* `webContentsId` Integer (optional)
* `webContents` WebContents (optional)
* `frame` WebFrameMain (optional)
* `frame` WebFrameMain | null (optional) - Requesting frame.
May be `null` if accessed after the frame has either navigated or been destroyed.
* `resourceType` string - Can be `mainFrame`, `subFrame`, `stylesheet`, `script`, `image`, `font`, `object`, `xhr`, `ping`, `cspReport`, `media`, `webSocket` or `other`.
* `referrer` string
* `timestamp` Double
@ -122,7 +124,8 @@ The `callback` has to be called with a `response` object.
* `method` string
* `webContentsId` Integer (optional)
* `webContents` WebContents (optional)
* `frame` WebFrameMain (optional)
* `frame` WebFrameMain | null (optional) - Requesting frame.
May be `null` if accessed after the frame has either navigated or been destroyed.
* `resourceType` string - Can be `mainFrame`, `subFrame`, `stylesheet`, `script`, `image`, `font`, `object`, `xhr`, `ping`, `cspReport`, `media`, `webSocket` or `other`.
* `referrer` string
* `timestamp` Double
@ -142,7 +145,8 @@ response are visible by the time this listener is fired.
* `method` string
* `webContentsId` Integer (optional)
* `webContents` WebContents (optional)
* `frame` WebFrameMain (optional)
* `frame` WebFrameMain | null (optional) - Requesting frame.
May be `null` if accessed after the frame has either navigated or been destroyed.
* `resourceType` string - Can be `mainFrame`, `subFrame`, `stylesheet`, `script`, `image`, `font`, `object`, `xhr`, `ping`, `cspReport`, `media`, `webSocket` or `other`.
* `referrer` string
* `timestamp` Double
@ -173,7 +177,8 @@ The `callback` has to be called with a `response` object.
* `method` string
* `webContentsId` Integer (optional)
* `webContents` WebContents (optional)
* `frame` WebFrameMain (optional)
* `frame` WebFrameMain | null (optional) - Requesting frame.
May be `null` if accessed after the frame has either navigated or been destroyed.
* `resourceType` string - Can be `mainFrame`, `subFrame`, `stylesheet`, `script`, `image`, `font`, `object`, `xhr`, `ping`, `cspReport`, `media`, `webSocket` or `other`.
* `referrer` string
* `timestamp` Double
@ -197,7 +202,8 @@ and response headers are available.
* `method` string
* `webContentsId` Integer (optional)
* `webContents` WebContents (optional)
* `frame` WebFrameMain (optional)
* `frame` WebFrameMain | null (optional) - Requesting frame.
May be `null` if accessed after the frame has either navigated or been destroyed.
* `resourceType` string - Can be `mainFrame`, `subFrame`, `stylesheet`, `script`, `image`, `font`, `object`, `xhr`, `ping`, `cspReport`, `media`, `webSocket` or `other`.
* `referrer` string
* `timestamp` Double
@ -222,7 +228,8 @@ redirect is about to occur.
* `method` string
* `webContentsId` Integer (optional)
* `webContents` WebContents (optional)
* `frame` WebFrameMain (optional)
* `frame` WebFrameMain | null (optional) - Requesting frame.
May be `null` if accessed after the frame has either navigated or been destroyed.
* `resourceType` string - Can be `mainFrame`, `subFrame`, `stylesheet`, `script`, `image`, `font`, `object`, `xhr`, `ping`, `cspReport`, `media`, `webSocket` or `other`.
* `referrer` string
* `timestamp` Double
@ -245,7 +252,8 @@ completed.
* `method` string
* `webContentsId` Integer (optional)
* `webContents` WebContents (optional)
* `frame` WebFrameMain (optional)
* `frame` WebFrameMain | null (optional) - Requesting frame.
May be `null` if accessed after the frame has either navigated or been destroyed.
* `resourceType` string - Can be `mainFrame`, `subFrame`, `stylesheet`, `script`, `image`, `font`, `object`, `xhr`, `ping`, `cspReport`, `media`, `webSocket` or `other`.
* `referrer` string
* `timestamp` Double

Просмотреть файл

@ -14,6 +14,34 @@ This document uses the following convention to categorize breaking changes:
## Planned Breaking API Changes (33.0)
### Behavior Changed: frame properties may retrieve detached WebFrameMain instances or none at all
APIs which provide access to a `WebFrameMain` instance may return an instance
with `frame.detached` set to `true`, or possibly return `null`.
When a frame performs a cross-origin navigation, it enters into a detached state
in which it's no longer attached to the page. In this state, it may be running
[unload](https://developer.mozilla.org/en-US/docs/Web/API/Window/unload_event)
handlers prior to being deleted. In the event of an IPC sent during this state,
`frame.detached` will be set to `true` with the frame being destroyed shortly
thereafter.
When receiving an event, it's important to access WebFrameMain properties
immediately upon being received. Otherwise, it's not guaranteed to point to the
same webpage as when received. To avoid misaligned expectations, Electron will
return `null` in the case of late access where the webpage has changed.
```ts
ipcMain.on('unload-event', (event) => {
event.senderFrame; // ✅ accessed immediately
});
ipcMain.on('unload-event', async (event) => {
await crossOriginNavigationPromise;
event.senderFrame; // ❌ returns `null` due to late access
});
```
### Behavior Changed: custom protocol URL handling on Windows
Due to changes made in Chromium to support [Non-Special Scheme URLs](http://bit.ly/url-non-special), custom protocol URLs that use Windows file paths will no longer work correctly with the deprecated `protocol.registerFileProtocol` and the `baseURLForDataURL` property on `BrowserWindow.loadURL`, `WebContents.loadURL`, and `<webview>.loadURL`. `protocol.handle` will also not work with these types of URLs but this is not a change since it has always worked that way.

Просмотреть файл

@ -485,10 +485,6 @@ const addReplyToEvent = (event: Electron.IpcMainEvent) => {
const addSenderToEvent = (event: Electron.IpcMainEvent | Electron.IpcMainInvokeEvent, sender: Electron.WebContents) => {
event.sender = sender;
const { processId, frameId } = event;
Object.defineProperty(event, 'senderFrame', {
get: () => webFrameMain.fromId(processId, frameId)
});
};
const addReturnValueToEvent = (event: Electron.IpcMainEvent) => {
@ -498,11 +494,6 @@ const addReturnValueToEvent = (event: Electron.IpcMainEvent) => {
});
};
const getWebFrameForEvent = (event: Electron.IpcMainEvent | Electron.IpcMainInvokeEvent) => {
if (!event.processId || !event.frameId) return null;
return webFrameMainBinding.fromIdOrNull(event.processId, event.frameId);
};
const commandLine = process._linkedBinding('electron_common_command_line');
const environment = process._linkedBinding('electron_common_environment');
@ -580,6 +571,28 @@ WebContents.prototype._init = function () {
enumerable: true
});
/**
* Cached IPC emitters sorted by dispatch priority.
* Caching is used to avoid frequent array allocations.
*
* 0: WebFrameMain ipc
* 1: WebContents ipc
* 2: ipcMain
*/
const cachedIpcEmitters: (ElectronInternal.IpcMainInternal | undefined)[] = [undefined, ipc, ipcMain];
// Get list of relevant IPC emitters for dispatch.
const getIpcEmittersForEvent = (event: Electron.IpcMainEvent | Electron.IpcMainInvokeEvent): (ElectronInternal.IpcMainInternal | undefined)[] => {
// Lookup by FrameTreeNode ID to ensure IPCs received after a frame swap are
// always received. This occurs when a RenderFrame sends an IPC while it's
// unloading and its internal state is pending deletion.
const { frameTreeNodeId } = event;
const webFrameByFtn = frameTreeNodeId ? webFrameMainBinding._fromFtnIdIfExists(frameTreeNodeId) : undefined;
cachedIpcEmitters[0] = webFrameByFtn?.ipc;
return cachedIpcEmitters;
};
// Add navigationHistory property which handles session history,
// maintaining a list of navigation entries for backward and forward navigation.
Object.defineProperty(this, 'navigationHistory', {
@ -610,10 +623,9 @@ WebContents.prototype._init = function () {
} else {
addReplyToEvent(event);
this.emit('ipc-message', event, channel, ...args);
const maybeWebFrame = getWebFrameForEvent(event);
maybeWebFrame && maybeWebFrame.ipc.emit(channel, event, ...args);
ipc.emit(channel, event, ...args);
ipcMain.emit(channel, event, ...args);
for (const ipcEmitter of getIpcEmittersForEvent(event)) {
ipcEmitter?.emit(channel, event, ...args);
}
}
});
@ -624,9 +636,8 @@ WebContents.prototype._init = function () {
console.error(`Error occurred in handler for '${channel}':`, error);
event._replyChannel.sendReply({ error: error.toString() });
};
const maybeWebFrame = getWebFrameForEvent(event);
const targets: (ElectronInternal.IpcMainInternal| undefined)[] = internal ? [ipcMainInternal] : [maybeWebFrame?.ipc, ipc, ipcMain];
const target = targets.find(target => target && (target as any)._invokeHandlers.has(channel));
const targets: (ElectronInternal.IpcMainInternal | undefined)[] = internal ? [ipcMainInternal] : getIpcEmittersForEvent(event);
const target = targets.find(target => (target as any)?._invokeHandlers.has(channel));
if (target) {
const handler = (target as any)._invokeHandlers.get(channel);
try {
@ -646,24 +657,27 @@ WebContents.prototype._init = function () {
ipcMainInternal.emit(channel, event, ...args);
} else {
addReplyToEvent(event);
const maybeWebFrame = getWebFrameForEvent(event);
if (this.listenerCount('ipc-message-sync') === 0 && ipc.listenerCount(channel) === 0 && ipcMain.listenerCount(channel) === 0 && (!maybeWebFrame || maybeWebFrame.ipc.listenerCount(channel) === 0)) {
const ipcEmitters = getIpcEmittersForEvent(event);
if (
this.listenerCount('ipc-message-sync') === 0 &&
ipcEmitters.every(emitter => !emitter || emitter.listenerCount(channel) === 0)
) {
console.warn(`WebContents #${this.id} called ipcRenderer.sendSync() with '${channel}' channel without listeners.`);
}
this.emit('ipc-message-sync', event, channel, ...args);
maybeWebFrame && maybeWebFrame.ipc.emit(channel, event, ...args);
ipc.emit(channel, event, ...args);
ipcMain.emit(channel, event, ...args);
for (const ipcEmitter of ipcEmitters) {
ipcEmitter?.emit(channel, event, ...args);
}
}
});
this.on('-ipc-ports', function (this: Electron.WebContents, event: Electron.IpcMainEvent, internal: boolean, channel: string, message: any, ports: any[]) {
addSenderToEvent(event, this);
event.ports = ports.map(p => new MessagePortMain(p));
const maybeWebFrame = getWebFrameForEvent(event);
maybeWebFrame && maybeWebFrame.ipc.emit(channel, event, message);
ipc.emit(channel, event, message);
ipcMain.emit(channel, event, message);
const ipcEmitters = getIpcEmittersForEvent(event);
for (const ipcEmitter of ipcEmitters) {
ipcEmitter?.emit(channel, event, message);
}
});
this.on('render-process-gone', (event, details) => {

Просмотреть файл

@ -168,8 +168,8 @@ const createGuest = function (embedder: Electron.WebContents, embedderFrameId: n
sendToEmbedder(IPC_MESSAGES.GUEST_VIEW_INTERNAL_DISPATCH_EVENT, 'will-frame-navigate', {
url: event.url,
isMainFrame: event.isMainFrame,
frameProcessId: event.frame.processId,
frameRoutingId: event.frame.routingId
frameProcessId: event.processId,
frameRoutingId: event.routingId
});
});

Просмотреть файл

@ -1689,7 +1689,8 @@ void WebContents::RenderFrameHostChanged(content::RenderFrameHost* old_host,
//
// |old_host| can be a nullptr so we use |new_host| for looking up the
// WebFrameMain instance.
auto* web_frame = WebFrameMain::FromRenderFrameHost(new_host);
auto* web_frame =
WebFrameMain::FromFrameTreeNodeId(new_host->GetFrameTreeNodeId());
if (web_frame) {
web_frame->UpdateRenderFrameHost(new_host);
}
@ -1861,6 +1862,8 @@ bool WebContents::EmitNavigationEvent(
dict.Set("url", url);
dict.Set("isSameDocument", is_same_document);
dict.Set("isMainFrame", is_main_frame);
dict.Set("processId", frame_process_id);
dict.Set("routingId", frame_routing_id);
dict.SetGetter("frame", frame_host);
dict.SetGetter("initiator", initiator_frame_host);
@ -1980,8 +1983,10 @@ gin::Handle<gin_helper::internal::Event> WebContents::MakeEventWithSender(
dict.Set("_replyChannel",
ReplyChannel::Create(isolate, std::move(callback)));
if (frame) {
dict.SetGetter("senderFrame", frame);
dict.Set("frameId", frame->GetRoutingID());
dict.Set("processId", frame->GetProcess()->GetID());
dict.Set("frameTreeNodeId", frame->GetFrameTreeNodeId());
}
return event;
}

Просмотреть файл

@ -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

Просмотреть файл

@ -13,6 +13,7 @@
#include "base/memory/weak_ptr.h"
#include "base/process/process.h"
#include "content/public/browser/frame_tree_node_id.h"
#include "content/public/browser/global_routing_id.h"
#include "gin/wrappable.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
@ -51,11 +52,10 @@ class WebFrameMain final : public gin::Wrappable<WebFrameMain>,
static gin::Handle<WebFrameMain> From(
v8::Isolate* isolate,
content::RenderFrameHost* render_frame_host);
static gin::Handle<WebFrameMain> FromOrNull(
v8::Isolate* isolate,
content::RenderFrameHost* render_frame_host);
static WebFrameMain* FromFrameTreeNodeId(
content::FrameTreeNodeId frame_tree_node_id);
static WebFrameMain* FromFrameToken(
content::GlobalRenderFrameHostToken frame_token);
static WebFrameMain* FromRenderFrameHost(
content::RenderFrameHost* render_frame_host);
@ -103,6 +103,7 @@ class WebFrameMain final : public gin::Wrappable<WebFrameMain>,
v8::Local<v8::Promise> ExecuteJavaScript(gin::Arguments* args,
const std::u16string& code);
bool Reload();
bool IsDestroyed() const;
void Send(v8::Isolate* isolate,
bool internal,
const std::string& channel,
@ -112,7 +113,8 @@ class WebFrameMain final : public gin::Wrappable<WebFrameMain>,
v8::Local<v8::Value> message_value,
std::optional<v8::Local<v8::Value>> transfer);
int FrameTreeNodeIDAsInt() const;
bool Detached() const;
content::FrameTreeNodeId FrameTreeNodeID() const;
std::string Name() const;
base::ProcessId OSProcessID() const;
int ProcessID() const;
@ -132,6 +134,7 @@ class WebFrameMain final : public gin::Wrappable<WebFrameMain>,
mojo::PendingReceiver<mojom::ElectronRenderer> pending_receiver_;
content::FrameTreeNodeId frame_tree_node_id_;
content::GlobalRenderFrameHostToken frame_token_;
raw_ptr<content::RenderFrameHost> render_frame_ = nullptr;
@ -139,6 +142,10 @@ class WebFrameMain final : public gin::Wrappable<WebFrameMain>,
// be accessed.
bool render_frame_disposed_ = false;
// Whether the content::RenderFrameHost is detached from the frame
// tree. This can occur while it's running unload handlers.
bool render_frame_detached_;
base::WeakPtrFactory<WebFrameMain> weak_factory_{this};
};

Просмотреть файл

@ -8,6 +8,7 @@
#include "content/public/browser/render_process_host.h"
#include "shell/browser/api/electron_api_web_frame_main.h"
#include "shell/common/gin_helper/accessor.h"
#include "shell/common/node_util.h"
namespace gin {
@ -17,6 +18,13 @@ v8::Persistent<v8::ObjectTemplate> rfh_templ;
} // namespace
// static
v8::Local<v8::Value> Converter<content::FrameTreeNodeId>::ToV8(
v8::Isolate* isolate,
const content::FrameTreeNodeId& val) {
return v8::Number::New(isolate, val.value());
}
// static
v8::Local<v8::Value> Converter<content::RenderFrameHost*>::ToV8(
v8::Isolate* isolate,
@ -94,8 +102,17 @@ bool Converter<gin_helper::AccessorValue<content::RenderFrameHost*>>::FromV8(
const int routing_id = routing_id_wrapper.As<v8::Number>()->Value();
auto* rfh = content::RenderFrameHost::FromID(process_id, routing_id);
if (!rfh)
return false;
if (!rfh) {
// Lazily evaluted property accessed after RFH has been destroyed.
// Continue to return nullptr, but emit warning to inform developers
// what occurred.
electron::util::EmitWarning(
isolate,
"Frame property was accessed after it navigated or was destroyed. "
"Avoid asynchronous tasks prior to indexing.",
"electron");
}
out->Value = rfh;
return true;

Просмотреть файл

@ -5,15 +5,23 @@
#ifndef ELECTRON_SHELL_COMMON_GIN_CONVERTERS_FRAME_CONVERTER_H_
#define ELECTRON_SHELL_COMMON_GIN_CONVERTERS_FRAME_CONVERTER_H_
#include "content/public/browser/frame_tree_node_id.h"
#include "gin/converter.h"
#include "shell/common/gin_helper/accessor.h"
namespace content {
class RenderFrameHost;
}
namespace gin {
template <>
struct Converter<content::FrameTreeNodeId> {
static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
const content::FrameTreeNodeId& val);
};
template <>
struct Converter<content::RenderFrameHost*> {
static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,

Просмотреть файл

@ -591,7 +591,7 @@ describe('BrowserWindow module', () => {
it('is triggered when a cross-origin iframe navigates _top', async () => {
w.loadURL(`data:text/html,<iframe src="http://127.0.0.1:${(server.address() as AddressInfo).port}/navigate-top"></iframe>`);
await emittedUntil(w.webContents, 'did-frame-finish-load', (e: any, isMainFrame: boolean) => !isMainFrame);
let initiator: WebFrameMain | undefined;
let initiator: WebFrameMain | null | undefined;
w.webContents.on('will-navigate', (e) => {
initiator = e.initiator;
});

Просмотреть файл

@ -835,5 +835,23 @@ describe('ipc module', () => {
const [, arg] = await once(w.webContents.mainFrame.frames[0].ipc, 'test');
expect(arg).to.equal(42);
});
it('receives ipcs from unloading frames in the main frame', async () => {
const server = http.createServer((req, res) => {
res.setHeader('content-type', 'text/html');
res.end('');
});
const { port } = await listen(server);
defer(() => {
server.close();
});
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
await w.loadURL(`http://localhost:${port}`);
await w.webContents.executeJavaScript('window.onunload = () => require(\'electron\').ipcRenderer.send(\'unload\'); void 0');
const onUnloadIpc = once(w.webContents.mainFrame.ipc, 'unload');
w.loadURL(`http://127.0.0.1:${port}`); // cross-origin navigation
const [{ senderFrame }] = await onUnloadIpc;
expect(senderFrame.detached).to.be.true();
});
});
});

Просмотреть файл

@ -18,10 +18,10 @@ describe('webFrameMain module', () => {
const fileUrl = (filename: string) => url.pathToFileURL(path.join(subframesPath, filename)).href;
type Server = { server: http.Server, url: string }
type Server = { server: http.Server, url: string, crossOriginUrl: string }
/** Creates an HTTP server whose handler embeds the given iframe src. */
const createServer = async () => {
const createServer = async (): Promise<Server> => {
const server = http.createServer((req, res) => {
const params = new URLSearchParams(new URL(req.url || '', `http://${req.headers.host}`).search || '');
if (params.has('frameSrc')) {
@ -30,7 +30,15 @@ describe('webFrameMain module', () => {
res.end('');
}
});
return { server, url: (await listen(server)).url + '/' };
const serverUrl = (await listen(server)).url + '/';
// HACK: Use 'localhost' instead of '127.0.0.1' so Chromium treats it as
// a separate origin because differing ports aren't enough 🤔
const crossOriginUrl = serverUrl.replace('127.0.0.1', 'localhost');
return {
server,
url: serverUrl,
crossOriginUrl
};
};
afterEach(closeAllWindows);
@ -285,8 +293,15 @@ describe('webFrameMain module', () => {
});
describe('RenderFrame lifespan', () => {
let server: Awaited<ReturnType<typeof createServer>>;
let w: BrowserWindow;
before(async () => {
server = await createServer();
});
after(() => {
server.server.close();
});
beforeEach(async () => {
w = new BrowserWindow({ show: false });
});
@ -302,19 +317,15 @@ describe('webFrameMain module', () => {
});
it('persists through cross-origin navigation', async () => {
const server = await createServer();
// 'localhost' is treated as a separate origin.
const crossOriginUrl = server.url.replace('127.0.0.1', 'localhost');
await w.loadURL(server.url);
const { mainFrame } = w.webContents;
expect(mainFrame.url).to.equal(server.url);
await w.loadURL(crossOriginUrl);
await w.loadURL(server.crossOriginUrl);
expect(w.webContents.mainFrame).to.equal(mainFrame);
expect(mainFrame.url).to.equal(crossOriginUrl);
expect(mainFrame.url).to.equal(server.crossOriginUrl);
});
it('recovers from renderer crash on same-origin', async () => {
const server = await createServer();
// Keep reference to mainFrame alive throughout crash and recovery.
const { mainFrame } = w.webContents;
await w.webContents.loadURL(server.url);
@ -328,9 +339,6 @@ describe('webFrameMain module', () => {
// Fixed by #34411
it('recovers from renderer crash on cross-origin', async () => {
const server = await createServer();
// 'localhost' is treated as a separate origin.
const crossOriginUrl = server.url.replace('127.0.0.1', 'localhost');
// Keep reference to mainFrame alive throughout crash and recovery.
const { mainFrame } = w.webContents;
await w.webContents.loadURL(server.url);
@ -339,10 +347,79 @@ describe('webFrameMain module', () => {
await crashEvent;
// A short wait seems to be required to reproduce the crash.
await setTimeout(100);
await w.webContents.loadURL(crossOriginUrl);
await w.webContents.loadURL(server.crossOriginUrl);
// Log just to keep mainFrame in scope.
console.log('mainFrame.url', mainFrame.url);
});
it('returns null upon accessing senderFrame after cross-origin navigation', async () => {
w = new BrowserWindow({
show: false,
webPreferences: {
preload: path.join(subframesPath, 'preload.js')
}
});
const preloadPromise = once(ipcMain, 'preload-ran');
await w.webContents.loadURL(server.url);
const [event] = await preloadPromise;
await w.webContents.loadURL(server.crossOriginUrl);
// senderFrame now points to a disposed RenderFrameHost. It should
// be null when attempting to access the lazily evaluated property.
expect(event.senderFrame).to.be.null();
});
it('is detached when unload handler sends IPC', async () => {
w = new BrowserWindow({
show: false,
webPreferences: {
preload: path.join(subframesPath, 'preload.js')
}
});
await w.webContents.loadURL(server.url);
const unloadPromise = new Promise<void>((resolve, reject) => {
ipcMain.once('preload-unload', (event) => {
try {
const { senderFrame } = event;
expect(senderFrame).to.not.be.null();
expect(senderFrame!.detached).to.be.true();
expect(senderFrame!.processId).to.equal(event.processId);
expect(senderFrame!.routingId).to.equal(event.frameId);
resolve();
} catch (error) {
reject(error);
}
});
});
await w.webContents.loadURL(server.crossOriginUrl);
await expect(unloadPromise).to.eventually.be.fulfilled();
});
it('disposes detached frame after cross-origin navigation', async () => {
w = new BrowserWindow({
show: false,
webPreferences: {
preload: path.join(subframesPath, 'preload.js')
}
});
await w.webContents.loadURL(server.url);
// eslint-disable-next-line prefer-const
let crossOriginPromise: Promise<void>;
const unloadPromise = new Promise<void>((resolve, reject) => {
ipcMain.once('preload-unload', async (event) => {
try {
const { senderFrame } = event;
expect(senderFrame!.detached).to.be.true();
await crossOriginPromise;
expect(() => senderFrame!.url).to.throw(/Render frame was disposed/);
resolve();
} catch (error) {
reject(error);
}
});
});
crossOriginPromise = w.webContents.loadURL(server.crossOriginUrl);
await expect(unloadPromise).to.eventually.be.fulfilled();
});
});
describe('webFrameMain.fromId', () => {
@ -388,10 +465,6 @@ describe('webFrameMain module', () => {
it('is not emitted upon cross-origin navigation', async () => {
const server = await createServer();
// HACK: Use 'localhost' instead of '127.0.0.1' so Chromium treats it as
// a separate origin because differing ports aren't enough 🤔
const secondUrl = server.url.replace('127.0.0.1', 'localhost');
const w = new BrowserWindow({ show: false });
await w.webContents.loadURL(server.url);
@ -401,7 +474,7 @@ describe('webFrameMain module', () => {
frameCreatedEmitted = true;
});
await w.webContents.loadURL(secondUrl);
await w.webContents.loadURL(server.crossOriginUrl);
expect(frameCreatedEmitted).to.be.false();
});
@ -419,8 +492,8 @@ describe('webFrameMain module', () => {
const w = new BrowserWindow({ show: false });
const promise = new Promise<void>(resolve => {
w.webContents.on('frame-created', (e, { frame }) => {
frame.on('dom-ready', () => {
if (frame.name === 'frameA') {
frame!.on('dom-ready', () => {
if (frame!.name === 'frameA') {
resolve();
}
});

4
spec/fixtures/sub-frames/preload.js поставляемый
Просмотреть файл

@ -7,3 +7,7 @@ ipcRenderer.send('preload-ran', window.location.href, webFrame.routingId);
ipcRenderer.on('preload-ping', () => {
ipcRenderer.send('preload-pong', webFrame.routingId);
});
window.addEventListener('unload', () => {
ipcRenderer.send('preload-unload', window.location.href);
});

3
typings/internal-ambient.d.ts поставляемый
Просмотреть файл

@ -125,7 +125,8 @@ declare namespace NodeJS {
interface WebFrameMainBinding {
WebFrameMain: typeof Electron.WebFrameMain;
fromId(processId: number, routingId: number): Electron.WebFrameMain;
fromIdOrNull(processId: number, routingId: number): Electron.WebFrameMain | null;
_fromIdIfExists(processId: number, routingId: number): Electron.WebFrameMain | null;
_fromFtnIdIfExists(frameTreeNodeId: number): Electron.WebFrameMain | null;
}
interface InternalWebPreferences {

7
typings/internal-electron.d.ts поставляемый
Просмотреть файл

@ -177,10 +177,12 @@ declare namespace Electron {
interface IpcMainEvent {
_replyChannel: ReplyChannel;
frameTreeNodeId?: number;
}
interface IpcMainInvokeEvent {
_replyChannel: ReplyChannel;
frameTreeNodeId?: number;
}
// Deprecated / undocumented BrowserWindow methods
@ -225,6 +227,11 @@ declare namespace Electron {
once(event: 'destroyed', listener: (event: Electron.Event) => void): this;
}
interface WebContentsWillFrameNavigateEventParams {
processId: number;
routingId: number;
}
}
declare namespace ElectronInternal {