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: MessagePorts in the main process #22404

Merged
merged 44 commits into from Mar 12, 2020
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
d9f47fd
wip
nornagon Oct 25, 2019
1d133f0
fix headers
nornagon Feb 25, 2020
d3f106c
refactor: split V8Serializer into its own file
nornagon Feb 25, 2020
b9e7f5f
write a blink version envelope for compatibility with MessagePort
nornagon Feb 26, 2020
69fcfe1
postmessage and onmessage in main proc
nornagon Feb 26, 2020
d7ea272
add a bunch of tests then make them pass
nornagon Feb 26, 2020
859f002
fix includes
nornagon Feb 27, 2020
3680442
fix WebContents.postMessage
nornagon Feb 27, 2020
2ba17bd
more robust unwrapping of BrowserSideMessagePort
nornagon Feb 27, 2020
c0dcdf9
lint
nornagon Feb 27, 2020
77ee2b8
docs
nornagon Feb 27, 2020
7ef22ac
fix patch
nornagon Feb 27, 2020
d87e0cd
cleanup
nornagon Feb 27, 2020
3beea42
robustify port forwarding test
nornagon Feb 28, 2020
9ee147a
move BrowserSideMessagePort to its own file
nornagon Feb 28, 2020
e3e8995
rename some methods for clarity
nornagon Feb 28, 2020
794039b
make 3rd argument to WebContents.postMessage actually optional
nornagon Feb 28, 2020
ba07cb2
allow creating message channels in the main process
nornagon Feb 28, 2020
bccd325
list MessageChannelMain in module-keys.js
nornagon Feb 28, 2020
64220e0
check errors when disentangling ports
nornagon Mar 3, 2020
01f47bb
prevent passing port to itself
nornagon Mar 3, 2020
ddd8585
flesh out documentation
nornagon Mar 3, 2020
3e60304
fix error for non-serializable message
nornagon Mar 3, 2020
66bfe8e
std::move
nornagon Mar 3, 2020
35fc782
add tests for closed ports
nornagon Mar 3, 2020
0cb0e68
fix GC behavior
nornagon Mar 4, 2020
2aff928
lint
nornagon Mar 4, 2020
98c5b09
docs lint
nornagon Mar 4, 2020
680e829
add missing handlescope :o
nornagon Mar 4, 2020
e617cd3
Update ipc-renderer.md
nornagon Mar 5, 2020
facca2c
Update web-contents.md
nornagon Mar 5, 2020
6dfa3f2
Update ipc-renderer.md
nornagon Mar 5, 2020
adb4037
use automatic argument conversion for WebContents::PostMessage
nornagon Mar 5, 2020
10be4a2
link to channel messaging api docs
nornagon Mar 5, 2020
197b409
also use automatic arg conversion in renderer_ipc
nornagon Mar 5, 2020
daabb9d
Update electron_api_renderer_ipc.cc
nornagon Mar 5, 2020
5a81f98
Update ipc-renderer.md
nornagon Mar 10, 2020
35c1201
use gin_helper::ErrorThrower
nornagon Mar 10, 2020
b1613c7
fix type in IpcBinding
nornagon Mar 10, 2020
888805f
fix build
nornagon Mar 10, 2020
2a1c026
Merge remote-tracking branch 'origin/master' into channel-messaging
nornagon Mar 10, 2020
99cd096
lint
nornagon Mar 10, 2020
20488a8
fix tests for updated error messages
nornagon Mar 10, 2020
9282360
Merge branch 'master' into channel-messaging
nornagon Mar 11, 2020
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
30 changes: 30 additions & 0 deletions docs/api/ipc-renderer.md
Expand Up @@ -143,6 +143,35 @@ Sends a message to a window with `webContentsId` via `channel`.
Like `ipcRenderer.send` but the event will be sent to the `<webview>` element in
the host page instead of the main process.

### `ipcRenderer.postMessage(channel, message, [options])`

* `channel` String
* `message` any
* `options` MessagePort[] (optional)
MarshallOfSound marked this conversation as resolved.
Show resolved Hide resolved

Send a message to the main process, optionally transferring ownership of zero
or more [`MessagePort`][] objects.

The transferred `MessagePort` objects will be available in the main process as
[`MessagePortMain`](message-port-main.md) objects by accessing the `ports`
property of the emitted event.

For example:
```js
// Renderer process
const { port1, port2 } = new MessageChannel
ipcRenderer.postMessage('port', {message: "hello"}, [port1])

// Main process
ipcMain.on('port', (e, msg) => {
const [port] = e.ports
// ...
})
```

For more information on using `MessagePort` and `MessageChannel`, see the [MDN
documentation](https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel).

## Event object

The documentation for the `event` object passed to the `callback` can be found
Expand All @@ -151,3 +180,4 @@ in the [`ipc-renderer-event`](structures/ipc-renderer-event.md) structure docs.
[event-emitter]: https://nodejs.org/api/events.html#events_class_eventemitter
[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
[`MessagePort`]: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort
26 changes: 26 additions & 0 deletions docs/api/message-channel.md
@@ -0,0 +1,26 @@
# MessageChannelMain

`MessageChannelMain` is the main-process-side equivalent of the DOM
[`MessageChannel`][] object. Its singular function is to create a pair of
connected [`MessagePortMain`](message-port-main.md) objects.

## Class: MessageChannelMain

Example:
```js
const { port1, port2 } = new MessageChannelMain
w.webContents.postMessage('port', null, [port2])
port1.postMessage({ some: 'message' })
```

### Instance Properties

#### `channel.port1`

A [`MessagePortMain`](message-port-main.md) property.

#### `channel.port2`

A [`MessagePortMain`](message-port-main.md) property.

[`MessageChannel`]: https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel
45 changes: 45 additions & 0 deletions docs/api/message-port-main.md
@@ -0,0 +1,45 @@
# MessagePortMain

`MessagePortMain` is the main-process-side equivalent of the DOM
[`MessagePort`][] object. It behaves similarly to the DOM version, with the
exception that it uses the Node.js `EventEmitter` event system, instead of the
DOM `EventTarget` system. This means you should use `port.on('message', ...)`
to listen for events, instead of `port.onmessage = ...` or
`port.addEventListener('message', ...)`

`MessagePortMain` is an [EventEmitter][event-emitter].

## Class: MessagePortMain

### Instance Methods

#### `port.postMessage(message, [transfer])`

* `message` any
* `transfer` MessagePortMain[] (optional)
nornagon marked this conversation as resolved.
Show resolved Hide resolved

Sends a message from the port, and optionally, transfers ownership of objects
to other browsing contexts.

#### `port.start()`

Starts the sending of messages queued on the port. Messages will be queued
until this method is called.

#### `port.close()`

Disconnects the port, so it is no longer active.

### Instance Events

#### Event: 'message'

Returns:

* `event` Object
* `data` any
* `ports` MessagePortMain[]
nornagon marked this conversation as resolved.
Show resolved Hide resolved

Emitted when a MessagePortMain object receives a message.

[`MessagePort`]: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort
1 change: 1 addition & 0 deletions docs/api/structures/ipc-main-event.md
Expand Up @@ -3,6 +3,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 - Returns the `webContents` that sent the message
* `ports` MessagePortMain[] - A list of MessagePorts that were transferred with this message
nornagon marked this conversation as resolved.
Show resolved Hide resolved
* `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
* `...args` any[]
1 change: 1 addition & 0 deletions docs/api/structures/ipc-renderer-event.md
Expand Up @@ -2,5 +2,6 @@

* `sender` IpcRenderer - The `IpcRenderer` instance that emitted the event originally
* `senderId` Integer - The `webContents.id` that sent the message, you can call `event.sender.sendTo(event.senderId, ...)` to reply to the message, see [ipcRenderer.sendTo][ipc-renderer-sendto] for more information. This only applies to messages sent from a different renderer. Messages sent directly from the main process set `event.senderId` to `0`.
* `ports` MessagePort[] - A list of MessagePorts that were transferred with this message

[ipc-renderer-sendto]: #ipcrenderersendtowindowid-channel--arg1-arg2-
26 changes: 26 additions & 0 deletions docs/api/web-contents.md
Expand Up @@ -1593,6 +1593,32 @@ ipcMain.on('ping', (event) => {
})
```

#### `contents.postMessage(channel, message, [options])`

* `channel` String
* `message` any
* `options` MessagePortMain[] (optional)
nornagon marked this conversation as resolved.
Show resolved Hide resolved

Send a message to the renderer process, optionally transferring ownership of
zero or more [`MessagePortMain`][] objects.

The transferred `MessagePortMain` objects will be available in the renderer
process by accessing the `ports` property of the emitted event. When they
arrive in the renderer, they will be native DOM `MessagePort` objects.

For example:
```js
// Main process
const { port1, port2 } = new MessageChannel
webContents.postMessage('port', {message: 'hello'}, [port1])

// Renderer process
ipcRenderer.on('port', (e, msg) => {
const [port] = e.ports
// ...
})
```

#### `contents.enableDeviceEmulation(parameters)`

* `parameters` Object
Expand Down
4 changes: 4 additions & 0 deletions filenames.auto.gni
Expand Up @@ -32,6 +32,8 @@ auto_filenames = {
"docs/api/locales.md",
"docs/api/menu-item.md",
"docs/api/menu.md",
"docs/api/message-channel.md",
"docs/api/message-port-main.md",
"docs/api/modernization",
"docs/api/native-image.md",
"docs/api/native-theme.md",
Expand Down Expand Up @@ -224,6 +226,7 @@ auto_filenames = {
"lib/browser/api/menu-item.js",
"lib/browser/api/menu-utils.js",
"lib/browser/api/menu.js",
"lib/browser/api/message-channel.ts",
"lib/browser/api/module-list.ts",
"lib/browser/api/native-theme.ts",
"lib/browser/api/net-log.js",
Expand Down Expand Up @@ -260,6 +263,7 @@ auto_filenames = {
"lib/browser/ipc-main-impl.ts",
"lib/browser/ipc-main-internal-utils.ts",
"lib/browser/ipc-main-internal.ts",
"lib/browser/message-port-main.ts",
"lib/browser/navigation-controller.js",
"lib/browser/remote/objects-registry.ts",
"lib/browser/remote/server.ts",
Expand Down
4 changes: 4 additions & 0 deletions filenames.gni
Expand Up @@ -121,6 +121,8 @@ filenames = {
"shell/browser/api/gpu_info_enumerator.h",
"shell/browser/api/gpuinfo_manager.cc",
"shell/browser/api/gpuinfo_manager.h",
"shell/browser/api/message_port.cc",
"shell/browser/api/message_port.h",
"shell/browser/api/process_metric.cc",
"shell/browser/api/process_metric.h",
"shell/browser/api/save_page_handler.cc",
Expand Down Expand Up @@ -558,6 +560,8 @@ filenames = {
"shell/common/skia_util.h",
"shell/common/v8_value_converter.cc",
"shell/common/v8_value_converter.h",
"shell/common/v8_value_serializer.cc",
"shell/common/v8_value_serializer.h",
"shell/common/world_ids.h",
"shell/renderer/api/context_bridge/render_frame_context_bridge_store.cc",
"shell/renderer/api/context_bridge/render_frame_context_bridge_store.h",
Expand Down
12 changes: 12 additions & 0 deletions lib/browser/api/message-channel.ts
@@ -0,0 +1,12 @@
import { MessagePortMain } from '@electron/internal/browser/message-port-main'
const { createPair } = process.electronBinding('message_port')

export default class MessageChannelMain {
port1: MessagePortMain;
port2: MessagePortMain;
constructor () {
const { port1, port2 } = createPair()
this.port1 = new MessagePortMain(port1)
this.port2 = new MessagePortMain(port2)
}
}
1 change: 1 addition & 0 deletions lib/browser/api/module-keys.js
Expand Up @@ -24,6 +24,7 @@ module.exports = [
{ name: 'nativeTheme' },
{ name: 'net' },
{ name: 'netLog' },
{ name: 'MessageChannelMain' },
{ name: 'Notification' },
{ name: 'powerMonitor' },
{ name: 'powerSaveBlocker' },
Expand Down
1 change: 1 addition & 0 deletions lib/browser/api/module-list.ts
Expand Up @@ -16,6 +16,7 @@ export const browserModuleList: ElectronInternal.ModuleEntry[] = [
{ name: 'inAppPurchase', loader: () => require('./in-app-purchase') },
{ name: 'Menu', loader: () => require('./menu') },
{ name: 'MenuItem', loader: () => require('./menu-item') },
{ name: 'MessageChannelMain', loader: () => require('./message-channel') },
{ name: 'nativeTheme', loader: () => require('./native-theme') },
{ name: 'net', loader: () => require('./net') },
{ name: 'netLog', loader: () => require('./net-log') },
Expand Down
13 changes: 13 additions & 0 deletions lib/browser/api/web-contents.js
Expand Up @@ -11,6 +11,7 @@ const { internalWindowOpen } = require('@electron/internal/browser/guest-window-
const NavigationController = require('@electron/internal/browser/navigation-controller')
const { ipcMainInternal } = require('@electron/internal/browser/ipc-main-internal')
const ipcMainUtils = require('@electron/internal/browser/ipc-main-internal-utils')
const { MessagePortMain } = require('@electron/internal/browser/message-port-main')

// session is not used here, the purpose is to make sure session is initalized
// before the webContents module.
Expand Down Expand Up @@ -115,6 +116,13 @@ WebContents.prototype.send = function (channel, ...args) {
return this._send(internal, sendToAll, channel, args)
}

WebContents.prototype.postMessage = function (...args) {
if (Array.isArray(args[2])) {
args[2] = args[2].map(o => o instanceof MessagePortMain ? o._internalPort : o)
}
this._postMessage(...args)
}

WebContents.prototype.sendToAll = function (channel, ...args) {
if (typeof channel !== 'string') {
throw new Error('Missing required channel argument')
Expand Down Expand Up @@ -472,6 +480,11 @@ WebContents.prototype._init = function () {
}
})

this.on('-ipc-ports', function (event, internal, channel, message, ports) {
event.ports = ports.map(p => new MessagePortMain(p))
ipcMain.emit(channel, event, message)
})

// Handle context menu action request from pepper plugin.
this.on('pepper-context-menu', function (event, params, callback) {
// Access Menu via electron.Menu to prevent circular require.
Expand Down
25 changes: 25 additions & 0 deletions lib/browser/message-port-main.ts
@@ -0,0 +1,25 @@
import { EventEmitter } from 'events'

export class MessagePortMain extends EventEmitter {
_internalPort: any
MarshallOfSound marked this conversation as resolved.
Show resolved Hide resolved
constructor (internalPort: any) {
super()
this._internalPort = internalPort
this._internalPort.emit = (channel: string, event: {ports: any[]}) => {
if (channel === 'message') { event = { ...event, ports: event.ports.map(p => new MessagePortMain(p)) } }
this.emit(channel, event)
}
}
start () {
return this._internalPort.start()
}
close () {
return this._internalPort.close()
}
postMessage (...args: any[]) {
if (Array.isArray(args[1])) {
args[1] = args[1].map((o: any) => o instanceof MessagePortMain ? o._internalPort : o)
}
return this._internalPort.postMessage(...args)
}
}
4 changes: 4 additions & 0 deletions lib/renderer/api/ipc-renderer.ts
Expand Up @@ -29,4 +29,8 @@ ipcRenderer.invoke = async function (channel, ...args) {
return result
}

ipcRenderer.postMessage = function (channel: string, message: any, transferables: any) {
return ipc.postMessage(channel, message, transferables)
}

export default ipcRenderer
4 changes: 2 additions & 2 deletions lib/renderer/init.ts
Expand Up @@ -45,9 +45,9 @@ v8Util.setHiddenValue(global, 'ipc', ipcEmitter)
v8Util.setHiddenValue(global, 'ipc-internal', ipcInternalEmitter)

v8Util.setHiddenValue(global, 'ipcNative', {
onMessage (internal: boolean, channel: string, args: any[], senderId: number) {
onMessage (internal: boolean, channel: string, ports: any[], args: any[], senderId: number) {
const sender = internal ? ipcInternalEmitter : ipcEmitter
sender.emit(channel, { sender, senderId }, ...args)
sender.emit(channel, { sender, senderId, ports }, ...args)
}
})

Expand Down
4 changes: 2 additions & 2 deletions lib/sandboxed_renderer/init.js
Expand Up @@ -54,9 +54,9 @@ const loadedModules = new Map([
// ElectronApiServiceImpl will look for the "ipcNative" hidden object when
// invoking the 'onMessage' callback.
v8Util.setHiddenValue(global, 'ipcNative', {
onMessage (internal, channel, args, senderId) {
onMessage (internal, channel, ports, args, senderId) {
const sender = internal ? ipcRendererInternal : electron.ipcRenderer
sender.emit(channel, { sender, senderId }, ...args)
sender.emit(channel, { sender, senderId, ports }, ...args)
}
})

Expand Down
1 change: 1 addition & 0 deletions patches/chromium/.patches
Expand Up @@ -73,6 +73,7 @@ expose_setuseragent_on_networkcontext.patch
feat_add_set_theme_source_to_allow_apps_to.patch
revert_cleanup_remove_menu_subtitles_sublabels.patch
export_fetchapi_mojo_traits_to_fix_component_build.patch
add_webmessageportconverter_entangleandinjectmessageportchannel.patch
MarshallOfSound marked this conversation as resolved.
Show resolved Hide resolved
revert_remove_contentrendererclient_shouldfork.patch
ignore_rc_check.patch
remove_usage_of_incognito_apis_in_the_spellchecker.patch
Expand Down
@@ -0,0 +1,51 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Jeremy Apthorp <nornagon@nornagon.net>
Date: Fri, 25 Oct 2019 11:23:03 -0700
Subject: add WebMessagePortConverter::EntangleAndInjectMessagePortChannel

This adds a method to the public Blink API that would otherwise require
accessing Blink internals. Its inverse, which already exists, is used in
Android WebView.

diff --git a/third_party/blink/public/web/web_message_port_converter.h b/third_party/blink/public/web/web_message_port_converter.h
index ad603aa7c557bfd4f571a541d70e2edf9ae757d9..d4b0bf8f5e8f3af9328b0099b65d9963414dfcc1 100644
--- a/third_party/blink/public/web/web_message_port_converter.h
+++ b/third_party/blink/public/web/web_message_port_converter.h
@@ -13,6 +13,7 @@ class Isolate;
template <class T>
class Local;
class Value;
+class Context;
} // namespace v8

namespace blink {
@@ -25,6 +26,9 @@ class WebMessagePortConverter {
// neutered, it will return nullopt.
BLINK_EXPORT static base::Optional<MessagePortChannel>
DisentangleAndExtractMessagePortChannel(v8::Isolate*, v8::Local<v8::Value>);
+
+ BLINK_EXPORT static v8::Local<v8::Value>
+ EntangleAndInjectMessagePortChannel(v8::Local<v8::Context>, MessagePortChannel);
};

} // namespace blink
diff --git a/third_party/blink/renderer/core/exported/web_message_port_converter.cc b/third_party/blink/renderer/core/exported/web_message_port_converter.cc
index 333760d667f6b98b3e7674bf9082f999743dadfa..fc2f517de1951380482fbfa92c038041e15d9c3e 100644
--- a/third_party/blink/renderer/core/exported/web_message_port_converter.cc
+++ b/third_party/blink/renderer/core/exported/web_message_port_converter.cc
@@ -21,4 +21,15 @@ WebMessagePortConverter::DisentangleAndExtractMessagePortChannel(
return port->Disentangle();
}

+v8::Local<v8::Value>
+WebMessagePortConverter::EntangleAndInjectMessagePortChannel(
+ v8::Local<v8::Context> context,
+ MessagePortChannel port_channel) {
+ auto* execution_context = ToExecutionContext(context);
+ CHECK(execution_context);
+ auto* port = MakeGarbageCollected<MessagePort>(*execution_context);
+ port->Entangle(std::move(port_channel));
+ return ToV8(port, context->Global(), context->GetIsolate());
+}
+
} // namespace blink