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 #34959

Merged
merged 9 commits into from Aug 3, 2022
9 changes: 9 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`](web-contents.md#contentsipc), 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), 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,11 @@ This corresponds to the [animationPolicy][] accessibility feature in Chromium.

### Instance Properties

#### `contents.ipc`

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

#### `contents.audioMuted`

A `boolean` property that determines whether this page is muted.
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;
22 changes: 16 additions & 6 deletions lib/browser/api/web-contents.ts
Expand Up @@ -9,6 +9,7 @@ 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.
Expand Down Expand Up @@ -563,6 +564,9 @@ WebContents.prototype._init = function () {

this._windowOpenHandler = null;

const ipc = new IpcMainImpl();
this.ipc = ipc;

// 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 @@ -571,6 +575,7 @@ WebContents.prototype._init = function () {
} else {
addReplyToEvent(event);
this.emit('ipc-message', event, channel, ...args);
ipc.emit(channel, event, ...args);
ipcMain.emit(channel, event, ...args);
}
});
Expand All @@ -582,11 +587,14 @@ 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)) {
(target as any)._invokeHandlers.get(channel)(event, ...args);
} else {
event._throw(`No handler registered for '${channel}'`);
const targets = internal ? [ipcMainInternal] : [ipc, ipcMain];
for (const target of targets) {
if ((target as any)._invokeHandlers.has(channel)) {
(target as any)._invokeHandlers.get(channel)(event, ...args);
break;
} else {
event._throw(`No handler registered for '${channel}'`);
}
}
});

Expand All @@ -597,17 +605,19 @@ WebContents.prototype._init = function () {
ipcMainInternal.emit(channel, event, ...args);
} else {
addReplyToEvent(event);
if (this.listenerCount('ipc-message-sync') === 0 && ipcMain.listenerCount(channel) === 0) {
if (this.listenerCount('ipc-message-sync') === 0 && ipc.listenerCount(channel) === 0 && ipcMain.listenerCount(channel) === 0) {
console.warn(`WebContents #${this.id} called ipcRenderer.sendSync() with '${channel}' channel without listeners.`);
}
this.emit('ipc-message-sync', event, channel, ...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);
ipcMain.emit(channel, event, message);
});

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', () => {});
67 changes: 64 additions & 3 deletions spec-main/api-ipc-spec.ts
Expand Up @@ -3,6 +3,7 @@ import { expect } from 'chai';
import { BrowserWindow, ipcMain, IpcMainInvokeEvent, MessageChannelMain, WebContents } from 'electron/main';
import { closeAllWindows } from './window-helpers';
import { emittedOnce } from './events-helpers';
import { defer } from './spec-helpers';

const v8Util = process._linkedBinding('electron_common_v8_util');

Expand Down Expand Up @@ -90,7 +91,7 @@ describe('ipc module', () => {
});

it('throws an error when invoking a handler that was removed', async () => {
ipcMain.handle('test', () => {});
ipcMain.handle('test', () => { });
ipcMain.removeHandler('test');
const done = new Promise<void>(resolve => ipcMain.once('result', (e, arg) => {
expect(arg.error).to.match(/No handler registered/);
Expand All @@ -101,9 +102,9 @@ describe('ipc module', () => {
});

it('forbids multiple handlers', async () => {
ipcMain.handle('test', () => {});
ipcMain.handle('test', () => { });
try {
expect(() => { ipcMain.handle('test', () => {}); }).to.throw(/second handler/);
expect(() => { ipcMain.handle('test', () => { }); }).to.throw(/second handler/);
} finally {
ipcMain.removeHandler('test');
}
Expand Down Expand Up @@ -563,4 +564,64 @@ describe('ipc module', () => {
generateTests('WebContents.postMessage', contents => contents.postMessage.bind(contents));
generateTests('WebFrameMain.postMessage', contents => contents.mainFrame.postMessage.bind(contents.mainFrame));
});

describe('WebContents.ipc', () => {
it('receives ipc messages sent from the WebContents', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.send(\'test\', 42)');
const [, num] = await emittedOnce(w.webContents.ipc, 'test');
expect(num).to.equal(42);
});

it('receives sync-ipc messages sent from the WebContents', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
w.webContents.ipc.on('test', (event, arg) => {
event.returnValue = arg * 2;
});
const result = await w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.sendSync(\'test\', 42)');
expect(result).to.equal(42 * 2);
});

it('receives postMessage messages sent from the WebContents, w/ MessagePorts', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.postMessage(\'test\', null, [(new MessageChannel).port1])');
const [event] = await emittedOnce(w.webContents.ipc, 'test');
expect(event.ports.length).to.equal(1);
});

it('handles invoke messages sent from the WebContents', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
w.webContents.ipc.handle('test', (_event, arg) => arg * 2);
const result = await w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.invoke(\'test\', 42)');
expect(result).to.equal(42 * 2);
});

it('cascades to ipcMain', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
let gotFromIpcMain = false;
const ipcMainReceived = new Promise<void>(resolve => ipcMain.on('test', () => { gotFromIpcMain = true; resolve(); }));
const ipcReceived = new Promise<boolean>(resolve => w.webContents.ipc.on('test', () => { resolve(gotFromIpcMain); }));
defer(() => ipcMain.removeAllListeners('test'));
w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.send(\'test\', 42)');

// assert that they are delivered in the correct order
expect(await ipcReceived).to.be.false();
await ipcMainReceived;
});

it('overrides ipcMain handlers', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
w.webContents.ipc.handle('test', (_event, arg) => arg * 2);
ipcMain.handle('test', () => { throw new Error('should not be called'); });
defer(() => ipcMain.removeHandler('test'));
const result = await w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.invoke(\'test\', 42)');
expect(result).to.equal(42 * 2);
});
});
});