Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add WebContents.ipc #35231

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
33 changes: 33 additions & 0 deletions docs/api/web-contents.md
Expand Up @@ -862,6 +862,8 @@ Returns:

Emitted when the renderer process sends an asynchronous message via `ipcRenderer.send()`.

See also [`webContents.ipc`](#contentsipc-readonly), which provides an [`IpcMain`](ipc-main.md)-like interface for responding to IPC messages specifically from this WebContents.

#### Event: 'ipc-message-sync'

Returns:
Expand All @@ -872,6 +874,8 @@ Returns:

Emitted when the renderer process sends a synchronous message via `ipcRenderer.sendSync()`.

See also [`webContents.ipc`](#contentsipc-readonly), which provides an [`IpcMain`](ipc-main.md)-like interface for responding to IPC messages specifically from this WebContents.

#### Event: 'preferred-size-changed'

Returns:
Expand Down Expand Up @@ -1985,6 +1989,35 @@ This corresponds to the [animationPolicy][] accessibility feature in Chromium.

### Instance Properties

#### `contents.ipc` _Readonly_

An [`IpcMain`](ipc-main.md) scoped to just IPC messages sent from this
WebContents.

IPC messages sent with `ipcRenderer.send`, `ipcRenderer.sendSync` or
`ipcRenderer.postMessage` will be delivered in the following order:

1. `contents.on('ipc-message')`
2. `contents.mainFrame.on(channel)`
3. `contents.ipc.on(channel)`
4. `ipcMain.on(channel)`

Handlers registered with `invoke` will be checked in the following order. The
first one that is defined will be called, the rest will be ignored.

1. `contents.mainFrame.handle(channel)`
2. `contents.handle(channel)`
3. `ipcMain.handle(channel)`

A handler or event listener registered on the WebContents will receive IPC
messages sent from any frame, including child frames. In most cases, only the
main frame can send IPC messages. However, if the `nodeIntegrationInSubFrames`
option is enabled, it is possible for child frames to send IPC messages also.
In that case, handlers should check the `senderFrame` property of the IPC event
to ensure that the message is coming from the expected frame. Alternatively,
register handlers on the appropriate frame directly using the
[`WebFrameMain.ipc`](web-frame-main.md#frameipc-readonly) interface.

#### `contents.audioMuted`

A `boolean` property that determines whether this page is muted.
Expand Down
25 changes: 25 additions & 0 deletions docs/api/web-frame-main.md
Expand Up @@ -140,6 +140,31 @@ ipcRenderer.on('port', (e, msg) => {

### Instance Properties

#### `frame.ipc` _Readonly_

An [`IpcMain`](ipc-main.md) instance scoped to the frame.

IPC messages sent with `ipcRenderer.send`, `ipcRenderer.sendSync` or
`ipcRenderer.postMessage` will be delivered in the following order:

1. `contents.on('ipc-message')`
2. `contents.mainFrame.on(channel)`
3. `contents.ipc.on(channel)`
4. `ipcMain.on(channel)`

Handlers registered with `invoke` will be checked in the following order. The
first one that is defined will be called, the rest will be ignored.

1. `contents.mainFrame.handle(channel)`
2. `contents.handle(channel)`
3. `ipcMain.handle(channel)`

In most cases, only the main frame of a WebContents can send or receive IPC
messages. However, if the `nodeIntegrationInSubFrames` option is enabled, it is
possible for child frames to send and receive IPC messages also. The
[`WebContents.ipc`](web-contents.md#contentsipc-readonly) interface may be more
convenient when `nodeIntegrationInSubFrames` is not enabled.

#### `frame.url` _Readonly_

A `string` representing the current URL of the frame.
Expand Down
3 changes: 0 additions & 3 deletions lib/browser/api/ipc-main.ts
Expand Up @@ -2,7 +2,4 @@ import { IpcMainImpl } from '@electron/internal/browser/ipc-main-impl';

const ipcMain = new IpcMainImpl();

// Do not throw exception when channel name is "error".
ipcMain.on('error', () => {});

export default ipcMain;
26 changes: 23 additions & 3 deletions lib/browser/api/web-contents.ts
Expand Up @@ -9,12 +9,15 @@ import { ipcMainInternal } from '@electron/internal/browser/ipc-main-internal';
import * as ipcMainUtils from '@electron/internal/browser/ipc-main-internal-utils';
import { MessagePortMain } from '@electron/internal/browser/message-port-main';
import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages';
import { IpcMainImpl } from '@electron/internal/browser/ipc-main-impl';

// session is not used here, the purpose is to make sure session is initialized
// before the webContents module.
// eslint-disable-next-line
session

const webFrameMainBinding = process._linkedBinding('electron_browser_web_frame_main');

let nextId = 0;
const getNextId = function () {
return ++nextId;
Expand Down Expand Up @@ -556,6 +559,12 @@ WebContents.prototype._init = function () {

this._windowOpenHandler = null;

const ipc = new IpcMainImpl();
Object.defineProperty(this, 'ipc', {
get () { return ipc; },
enumerable: true
});

// Dispatch IPC messages to the ipc module.
this.on('-ipc-message' as any, function (this: Electron.WebContents, event: Electron.IpcMainEvent, internal: boolean, channel: string, args: any[]) {
addSenderFrameToEvent(event);
Expand All @@ -564,6 +573,9 @@ WebContents.prototype._init = function () {
} else {
addReplyToEvent(event);
this.emit('ipc-message', event, channel, ...args);
const maybeWebFrame = webFrameMainBinding.fromIdOrNull(event.processId, event.frameId);
maybeWebFrame && maybeWebFrame.ipc.emit(channel, event, ...args);
ipc.emit(channel, event, ...args);
ipcMain.emit(channel, event, ...args);
}
});
Expand All @@ -575,8 +587,10 @@ WebContents.prototype._init = function () {
console.error(`Error occurred in handler for '${channel}':`, error);
event.sendReply({ error: error.toString() });
};
const target = internal ? ipcMainInternal : ipcMain;
if ((target as any)._invokeHandlers.has(channel)) {
const maybeWebFrame = webFrameMainBinding.fromIdOrNull(event.processId, event.frameId);
const targets: (ElectronInternal.IpcMainInternal| undefined)[] = internal ? [ipcMainInternal] : [maybeWebFrame && maybeWebFrame.ipc, ipc, ipcMain];
const target = targets.find(target => target && (target as any)._invokeHandlers.has(channel));
if (target) {
(target as any)._invokeHandlers.get(channel)(event, ...args);
} else {
event._throw(`No handler registered for '${channel}'`);
Expand All @@ -590,17 +604,23 @@ WebContents.prototype._init = function () {
ipcMainInternal.emit(channel, event, ...args);
} else {
addReplyToEvent(event);
if (this.listenerCount('ipc-message-sync') === 0 && ipcMain.listenerCount(channel) === 0) {
const maybeWebFrame = webFrameMainBinding.fromIdOrNull(event.processId, event.frameId);
if (this.listenerCount('ipc-message-sync') === 0 && ipc.listenerCount(channel) === 0 && ipcMain.listenerCount(channel) === 0 && (!maybeWebFrame || maybeWebFrame.ipc.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);
}
});

this.on('-ipc-ports' as any, function (event: Electron.IpcMainEvent, internal: boolean, channel: string, message: any, ports: any[]) {
addSenderFrameToEvent(event);
event.ports = ports.map(p => new MessagePortMain(p));
ipc.emit(channel, event, message);
const maybeWebFrame = webFrameMainBinding.fromIdOrNull(event.processId, event.frameId);
maybeWebFrame && maybeWebFrame.ipc.emit(channel, event, message);
ipcMain.emit(channel, event, message);
});

Expand Down
9 changes: 9 additions & 0 deletions lib/browser/api/web-frame-main.ts
@@ -1,7 +1,16 @@
import { MessagePortMain } from '@electron/internal/browser/message-port-main';
import { IpcMainImpl } from '@electron/internal/browser/ipc-main-impl';

const { WebFrameMain, fromId } = process._linkedBinding('electron_browser_web_frame_main');

Object.defineProperty(WebFrameMain.prototype, 'ipc', {
get () {
const ipc = new IpcMainImpl();
Object.defineProperty(this, 'ipc', { value: ipc });
return ipc;
}
});

WebFrameMain.prototype.send = function (channel, ...args) {
if (typeof channel !== 'string') {
throw new Error('Missing required channel argument');
Expand Down
7 changes: 7 additions & 0 deletions lib/browser/ipc-main-impl.ts
Expand Up @@ -4,6 +4,13 @@ import { IpcMainInvokeEvent } from 'electron/main';
export class IpcMainImpl extends EventEmitter {
private _invokeHandlers: Map<string, (e: IpcMainInvokeEvent, ...args: any[]) => void> = new Map();

constructor () {
super();

// Do not throw exception when channel name is "error".
this.on('error', () => {});
}

handle: Electron.IpcMain['handle'] = (method, fn) => {
if (this._invokeHandlers.has(method)) {
throw new Error(`Attempted to register a second handler for '${method}'`);
Expand Down
3 changes: 0 additions & 3 deletions lib/browser/ipc-main-internal.ts
@@ -1,6 +1,3 @@
import { IpcMainImpl } from '@electron/internal/browser/ipc-main-impl';

export const ipcMainInternal = new IpcMainImpl() as ElectronInternal.IpcMainInternal;

// Do not throw exception when channel name is "error".
ipcMainInternal.on('error', () => {});
27 changes: 27 additions & 0 deletions shell/browser/api/electron_api_web_frame_main.cc
Expand Up @@ -362,6 +362,18 @@ gin::Handle<WebFrameMain> WebFrameMain::From(v8::Isolate* isolate,
return handle;
}

// static
gin::Handle<WebFrameMain> WebFrameMain::FromOrNull(
v8::Isolate* isolate,
content::RenderFrameHost* rfh) {
if (rfh == nullptr)
return gin::Handle<WebFrameMain>();
auto* web_frame = FromRenderFrameHost(rfh);
if (web_frame)
return gin::CreateHandle(isolate, web_frame);
return gin::Handle<WebFrameMain>();
}

// static
v8::Local<v8::ObjectTemplate> WebFrameMain::FillObjectTemplate(
v8::Isolate* isolate,
Expand Down Expand Up @@ -409,6 +421,20 @@ v8::Local<v8::Value> FromID(gin_helper::ErrorThrower thrower,
return WebFrameMain::From(thrower.isolate(), rfh).ToV8();
}

v8::Local<v8::Value> FromIDOrNull(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::FromID(render_process_id, render_frame_id);

return WebFrameMain::FromOrNull(thrower.isolate(), rfh).ToV8();
}

void Initialize(v8::Local<v8::Object> exports,
v8::Local<v8::Value> unused,
v8::Local<v8::Context> context,
Expand All @@ -417,6 +443,7 @@ 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);
}

} // namespace
Expand Down
3 changes: 3 additions & 0 deletions shell/browser/api/electron_api_web_frame_main.h
Expand Up @@ -44,6 +44,9 @@ class WebFrameMain : 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(int frame_tree_node_id);
static WebFrameMain* FromRenderFrameHost(
content::RenderFrameHost* render_frame_host);
Expand Down