diff --git a/docs/api/ipc-renderer.md b/docs/api/ipc-renderer.md index 2245761733ed3..a47d408429507 100644 --- a/docs/api/ipc-renderer.md +++ b/docs/api/ipc-renderer.md @@ -57,7 +57,7 @@ Removes all listeners, or those of the specified `channel`. Send an asynchronous message to the main process via `channel`, along with arguments. Arguments will be serialized with the [Structured Clone -Algorithm][SCA], just like [`postMessage`][], so prototype chains will not be +Algorithm][SCA], just like [`window.postMessage`][], so prototype chains will not be included. Sending Functions, Promises, Symbols, WeakMaps, or WeakSets will throw an exception. @@ -68,6 +68,10 @@ throw an exception. The main process handles it by listening for `channel` with the [`ipcMain`](ipc-main.md) module. +If you need to transfer a [`MessagePort`][] to the main process, use [`ipcRenderer.postMessage`](#ipcrendererpostmessagechannel-message-transfer). + +If you want to receive a single response from the main process, like the result of a method call, consider using [`ipcRenderer.invoke`](#ipcrendererinvokechannel-args). + ### `ipcRenderer.invoke(channel, ...args)` * `channel` String @@ -77,7 +81,7 @@ Returns `Promise` - Resolves with the response from the main process. Send a message to the main process via `channel` and expect a result asynchronously. Arguments will be serialized with the [Structured Clone -Algorithm][SCA], just like [`postMessage`][], so prototype chains will not be +Algorithm][SCA], just like [`window.postMessage`][], so prototype chains will not be included. Sending Functions, Promises, Symbols, WeakMaps, or WeakSets will throw an exception. @@ -102,6 +106,10 @@ ipcMain.handle('some-name', async (event, someArgument) => { }) ``` +If you need to transfer a [`MessagePort`][] to the main process, use [`ipcRenderer.postMessage`](#ipcrendererpostmessagechannel-message-transfer). + +If you do not need a respons to the message, consider using [`ipcRenderer.send`](#ipcrenderersendchannel-args). + ### `ipcRenderer.sendSync(channel, ...args)` * `channel` String @@ -111,7 +119,7 @@ Returns `any` - The value sent back by the [`ipcMain`](ipc-main.md) handler. Send a message to the main process via `channel` and expect a result synchronously. Arguments will be serialized with the [Structured Clone -Algorithm][SCA], just like [`postMessage`][], so prototype chains will not be +Algorithm][SCA], just like [`window.postMessage`][], so prototype chains will not be included. Sending Functions, Promises, Symbols, WeakMaps, or WeakSets will throw an exception. @@ -127,6 +135,35 @@ and replies by setting `event.returnValue`. > last resort. It's much better to use the asynchronous version, > [`invoke()`](ipc-renderer.md#ipcrendererinvokechannel-args). +### `ipcRenderer.postMessage(channel, message, [transfer])` + +* `channel` String +* `message` any +* `transfer` MessagePort[] (optional) + +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). + ### `ipcRenderer.sendTo(webContentsId, channel, ...args)` * `webContentsId` Number @@ -150,4 +187,5 @@ 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 +[`window.postMessage`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage +[`MessagePort`]: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort diff --git a/docs/api/message-channel-main.md b/docs/api/message-channel-main.md new file mode 100644 index 0000000000000..be345ed76b610 --- /dev/null +++ b/docs/api/message-channel-main.md @@ -0,0 +1,30 @@ +# 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. + +See the [Channel Messaging API][] documentation for more information on using +channel messaging. + +## 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 +[Channel Messaging API]: https://developer.mozilla.org/en-US/docs/Web/API/Channel_Messaging_API diff --git a/docs/api/message-port-main.md b/docs/api/message-port-main.md new file mode 100644 index 0000000000000..2b1d528ab1d63 --- /dev/null +++ b/docs/api/message-port-main.md @@ -0,0 +1,49 @@ +# 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', ...)` + +See the [Channel Messaging API][] documentation for more information on using +channel messaging. + +`MessagePortMain` is an [EventEmitter][event-emitter]. + +## Class: MessagePortMain + +### Instance Methods + +#### `port.postMessage(message, [transfer])` + +* `message` any +* `transfer` MessagePortMain[] (optional) + +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: + +* `messageEvent` Object + * `data` any + * `ports` MessagePortMain[] + +Emitted when a MessagePortMain object receives a message. + +[`MessagePort`]: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort +[Channel Messaging API]: https://developer.mozilla.org/en-US/docs/Web/API/Channel_Messaging_API diff --git a/docs/api/structures/ipc-main-event.md b/docs/api/structures/ipc-main-event.md index 2bab5d1a240fa..f222de35b86dc 100644 --- a/docs/api/structures/ipc-main-event.md +++ b/docs/api/structures/ipc-main-event.md @@ -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 * `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[] diff --git a/docs/api/structures/ipc-renderer-event.md b/docs/api/structures/ipc-renderer-event.md index 6f56d8cf3f677..4b2c0805ce21a 100644 --- a/docs/api/structures/ipc-renderer-event.md +++ b/docs/api/structures/ipc-renderer-event.md @@ -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- diff --git a/docs/api/web-contents.md b/docs/api/web-contents.md index 9662d75d13c91..6cd02149e128a 100644 --- a/docs/api/web-contents.md +++ b/docs/api/web-contents.md @@ -1580,6 +1580,32 @@ ipcMain.on('ping', (event) => { }) ``` +#### `contents.postMessage(channel, message, [transfer])` + +* `channel` String +* `message` any +* `transfer` MessagePortMain[] (optional) + +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 MessageChannelMain() +webContents.postMessage('port', { message: 'hello' }, [port1]) + +// Renderer process +ipcRenderer.on('port', (e, msg) => { + const [port] = e.ports + // ... +}) +``` + #### `contents.enableDeviceEmulation(parameters)` * `parameters` Object diff --git a/filenames.auto.gni b/filenames.auto.gni index 2ec867ea4e705..8bb8c52abef10 100644 --- a/filenames.auto.gni +++ b/filenames.auto.gni @@ -32,6 +32,8 @@ auto_filenames = { "docs/api/locales.md", "docs/api/menu-item.md", "docs/api/menu.md", + "docs/api/message-channel-main.md", + "docs/api/message-port-main.md", "docs/api/modernization", "docs/api/native-image.md", "docs/api/native-theme.md", @@ -223,6 +225,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", @@ -258,6 +261,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", diff --git a/filenames.gni b/filenames.gni index 0c7bac9e1904e..7b1e3fbe47380 100644 --- a/filenames.gni +++ b/filenames.gni @@ -125,6 +125,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", @@ -510,6 +512,7 @@ filenames = { "shell/common/gin_helper/event_emitter_caller.h", "shell/common/gin_helper/function_template.cc", "shell/common/gin_helper/function_template.h", + "shell/common/gin_helper/function_template_extensions.h", "shell/common/gin_helper/locker.cc", "shell/common/gin_helper/locker.h", "shell/common/gin_helper/microtasks_scope.cc", @@ -561,6 +564,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/object_cache.cc", "shell/renderer/api/context_bridge/object_cache.h", diff --git a/lib/browser/api/message-channel.ts b/lib/browser/api/message-channel.ts new file mode 100644 index 0000000000000..bcaf7cbc4e6c6 --- /dev/null +++ b/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); + } +} diff --git a/lib/browser/api/module-list.ts b/lib/browser/api/module-list.ts index 307898dee797f..814fdabd13f87 100644 --- a/lib/browser/api/module-list.ts +++ b/lib/browser/api/module-list.ts @@ -14,6 +14,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') }, diff --git a/lib/browser/api/module-names.ts b/lib/browser/api/module-names.ts index 614b86029f1a7..3d38360c5adff 100644 --- a/lib/browser/api/module-names.ts +++ b/lib/browser/api/module-names.ts @@ -20,6 +20,7 @@ export const browserModuleNames = [ 'nativeTheme', 'net', 'netLog', + 'MessageChannelMain', 'Notification', 'powerMonitor', 'powerSaveBlocker', diff --git a/lib/browser/api/web-contents.js b/lib/browser/api/web-contents.js index d8a589e1f6cf9..8e99ca708aadd 100644 --- a/lib/browser/api/web-contents.js +++ b/lib/browser/api/web-contents.js @@ -10,6 +10,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. @@ -114,6 +115,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'); @@ -471,6 +479,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. diff --git a/lib/browser/message-port-main.ts b/lib/browser/message-port-main.ts new file mode 100644 index 0000000000000..234b5f2f3b969 --- /dev/null +++ b/lib/browser/message-port-main.ts @@ -0,0 +1,25 @@ +import { EventEmitter } from 'events'; + +export class MessagePortMain extends EventEmitter { + _internalPort: any + 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); + } +} diff --git a/lib/renderer/api/ipc-renderer.ts b/lib/renderer/api/ipc-renderer.ts index a7f675e335dc8..b66998179df2d 100644 --- a/lib/renderer/api/ipc-renderer.ts +++ b/lib/renderer/api/ipc-renderer.ts @@ -31,4 +31,8 @@ if (!ipcRenderer.send) { }; } +ipcRenderer.postMessage = function (channel: string, message: any, transferables: any) { + return ipc.postMessage(channel, message, transferables); +}; + export default ipcRenderer; diff --git a/lib/renderer/init.ts b/lib/renderer/init.ts index bd8b6e79b7eb1..0414785a8b215 100644 --- a/lib/renderer/init.ts +++ b/lib/renderer/init.ts @@ -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); } }); diff --git a/lib/sandboxed_renderer/init.js b/lib/sandboxed_renderer/init.js index d6c139c645941..f045b64487f24 100644 --- a/lib/sandboxed_renderer/init.js +++ b/lib/sandboxed_renderer/init.js @@ -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); } }); diff --git a/patches/chromium/.patches b/patches/chromium/.patches index 059bb9dd0d88d..9130e73bb549c 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -71,6 +71,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 revert_remove_contentrendererclient_shouldfork.patch ignore_rc_check.patch remove_usage_of_incognito_apis_in_the_spellchecker.patch diff --git a/patches/chromium/add_webmessageportconverter_entangleandinjectmessageportchannel.patch b/patches/chromium/add_webmessageportconverter_entangleandinjectmessageportchannel.patch new file mode 100644 index 0000000000000..09d5e83acb8a4 --- /dev/null +++ b/patches/chromium/add_webmessageportconverter_entangleandinjectmessageportchannel.patch @@ -0,0 +1,51 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jeremy Apthorp +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 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 + DisentangleAndExtractMessagePortChannel(v8::Isolate*, v8::Local); ++ ++ BLINK_EXPORT static v8::Local ++ EntangleAndInjectMessagePortChannel(v8::Local, 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 ++WebMessagePortConverter::EntangleAndInjectMessagePortChannel( ++ v8::Local context, ++ MessagePortChannel port_channel) { ++ auto* execution_context = ToExecutionContext(context); ++ CHECK(execution_context); ++ auto* port = MakeGarbageCollected(*execution_context); ++ port->Entangle(std::move(port_channel)); ++ return ToV8(port, context->Global(), context->GetIsolate()); ++} ++ + } // namespace blink diff --git a/shell/browser/api/electron_api_web_contents.cc b/shell/browser/api/electron_api_web_contents.cc index ae0f2d9529c5c..d7a0fb2fdf5cd 100644 --- a/shell/browser/api/electron_api_web_contents.cc +++ b/shell/browser/api/electron_api_web_contents.cc @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -45,12 +46,17 @@ #include "content/public/browser/web_contents.h" #include "electron/buildflags/buildflags.h" #include "electron/shell/common/api/api.mojom.h" +#include "gin/data_object_builder.h" +#include "gin/handle.h" +#include "gin/object_template_builder.h" +#include "gin/wrappable.h" #include "mojo/public/cpp/bindings/associated_remote.h" #include "mojo/public/cpp/system/platform_handle.h" #include "ppapi/buildflags/buildflags.h" #include "shell/browser/api/electron_api_browser_window.h" #include "shell/browser/api/electron_api_debugger.h" #include "shell/browser/api/electron_api_session.h" +#include "shell/browser/api/message_port.h" #include "shell/browser/browser.h" #include "shell/browser/child_web_contents_tracker.h" #include "shell/browser/electron_autofill_driver_factory.h" @@ -86,8 +92,10 @@ #include "shell/common/mouse_util.h" #include "shell/common/node_includes.h" #include "shell/common/options_switches.h" +#include "shell/common/v8_value_serializer.h" #include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h" #include "third_party/blink/public/common/input/web_input_event.h" +#include "third_party/blink/public/common/messaging/transferable_message_mojom_traits.h" #include "third_party/blink/public/common/page/page_zoom.h" #include "third_party/blink/public/mojom/favicon/favicon_url.mojom.h" #include "third_party/blink/public/mojom/frame/find_in_page.mojom.h" @@ -1134,6 +1142,49 @@ void WebContents::Invoke(bool internal, std::move(callback), internal, channel, std::move(arguments)); } +void WebContents::ReceivePostMessage(const std::string& channel, + blink::TransferableMessage message) { + v8::HandleScope handle_scope(isolate()); + auto wrapped_ports = + MessagePort::EntanglePorts(isolate(), std::move(message.ports)); + v8::Local message_value = + electron::DeserializeV8Value(isolate(), message); + EmitWithSender("-ipc-ports", bindings_.dispatch_context(), InvokeCallback(), + false, channel, message_value, std::move(wrapped_ports)); +} + +void WebContents::PostMessage(const std::string& channel, + v8::Local message_value, + base::Optional> transfer) { + blink::TransferableMessage transferable_message; + if (!electron::SerializeV8Value(isolate(), message_value, + &transferable_message)) { + // SerializeV8Value sets an exception. + return; + } + + std::vector> wrapped_ports; + if (transfer) { + if (!gin::ConvertFromV8(isolate(), *transfer, &wrapped_ports)) { + isolate()->ThrowException(v8::Exception::Error( + gin::StringToV8(isolate(), "Invalid value for transfer"))); + return; + } + } + + bool threw_exception = false; + transferable_message.ports = + MessagePort::DisentanglePorts(isolate(), wrapped_ports, &threw_exception); + if (threw_exception) + return; + + content::RenderFrameHost* frame_host = web_contents()->GetMainFrame(); + mojo::AssociatedRemote electron_renderer; + frame_host->GetRemoteAssociatedInterfaces()->GetInterface(&electron_renderer); + electron_renderer->ReceivePostMessage(channel, + std::move(transferable_message)); +} + void WebContents::MessageSync(bool internal, const std::string& channel, blink::CloneableMessage arguments, @@ -2700,6 +2751,7 @@ void WebContents::BuildPrototype(v8::Isolate* isolate, .SetMethod("isFocused", &WebContents::IsFocused) .SetMethod("tabTraverse", &WebContents::TabTraverse) .SetMethod("_send", &WebContents::SendIPCMessage) + .SetMethod("_postMessage", &WebContents::PostMessage) .SetMethod("_sendToFrame", &WebContents::SendIPCMessageToFrame) .SetMethod("sendInputEvent", &WebContents::SendInputEvent) .SetMethod("beginFrameSubscription", &WebContents::BeginFrameSubscription) diff --git a/shell/browser/api/electron_api_web_contents.h b/shell/browser/api/electron_api_web_contents.h index 6cf41e919330a..349c4163fa427 100644 --- a/shell/browser/api/electron_api_web_contents.h +++ b/shell/browser/api/electron_api_web_contents.h @@ -253,6 +253,10 @@ class WebContents : public gin_helper::TrackableObject, const std::string& channel, v8::Local args); + void PostMessage(const std::string& channel, + v8::Local message, + base::Optional> transfer); + // Send WebInputEvent to the page. void SendInputEvent(v8::Isolate* isolate, v8::Local input_event); @@ -537,6 +541,8 @@ class WebContents : public gin_helper::TrackableObject, const std::string& channel, blink::CloneableMessage arguments, InvokeCallback callback) override; + void ReceivePostMessage(const std::string& channel, + blink::TransferableMessage message) override; void MessageSync(bool internal, const std::string& channel, blink::CloneableMessage arguments, diff --git a/shell/browser/api/message_port.cc b/shell/browser/api/message_port.cc new file mode 100644 index 0000000000000..16673beb08d83 --- /dev/null +++ b/shell/browser/api/message_port.cc @@ -0,0 +1,276 @@ +// Copyright (c) 2020 Slack Technologies, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "shell/browser/api/message_port.h" + +#include +#include +#include + +#include "base/strings/string_number_conversions.h" +#include "gin/arguments.h" +#include "gin/data_object_builder.h" +#include "gin/handle.h" +#include "gin/object_template_builder.h" +#include "shell/common/gin_helper/dictionary.h" +#include "shell/common/gin_helper/error_thrower.h" +#include "shell/common/gin_helper/event_emitter_caller.h" +#include "shell/common/node_includes.h" +#include "shell/common/v8_value_serializer.h" +#include "third_party/blink/public/common/messaging/transferable_message.h" +#include "third_party/blink/public/common/messaging/transferable_message_mojom_traits.h" +#include "third_party/blink/public/mojom/messaging/transferable_message.mojom.h" + +namespace electron { + +gin::WrapperInfo MessagePort::kWrapperInfo = {gin::kEmbedderNativeGin}; + +MessagePort::MessagePort() = default; +MessagePort::~MessagePort() = default; + +// static +gin::Handle MessagePort::Create(v8::Isolate* isolate) { + return gin::CreateHandle(isolate, new MessagePort()); +} + +void MessagePort::PostMessage(gin::Arguments* args) { + if (!IsEntangled()) + return; + DCHECK(!IsNeutered()); + + blink::TransferableMessage transferable_message; + + v8::Local message_value; + if (!args->GetNext(&message_value)) { + args->ThrowTypeError("Expected at least one argument to postMessage"); + return; + } + + electron::SerializeV8Value(args->isolate(), message_value, + &transferable_message); + + v8::Local transferables; + std::vector> wrapped_ports; + if (args->GetNext(&transferables)) { + if (!gin::ConvertFromV8(args->isolate(), transferables, &wrapped_ports)) { + args->ThrowError(); + return; + } + } + + // Make sure we aren't connected to any of the passed-in ports. + for (unsigned i = 0; i < wrapped_ports.size(); ++i) { + if (wrapped_ports[i].get() == this) { + gin_helper::ErrorThrower(args->isolate()) + .ThrowError("Port at index " + base::NumberToString(i) + + " contains the source port."); + return; + } + } + + bool threw_exception = false; + transferable_message.ports = MessagePort::DisentanglePorts( + args->isolate(), wrapped_ports, &threw_exception); + if (threw_exception) + return; + + mojo::Message mojo_message = blink::mojom::TransferableMessage::WrapAsMessage( + std::move(transferable_message)); + connector_->Accept(&mojo_message); +} + +void MessagePort::Start() { + if (!IsEntangled()) + return; + + if (started_) + return; + + started_ = true; + if (HasPendingActivity()) + Pin(); + connector_->ResumeIncomingMethodCallProcessing(); +} + +void MessagePort::Close() { + if (closed_) + return; + if (!IsNeutered()) { + connector_ = nullptr; + Entangle(mojo::MessagePipe().handle0); + } + closed_ = true; + if (!HasPendingActivity()) + Unpin(); +} + +void MessagePort::Entangle(mojo::ScopedMessagePipeHandle handle) { + DCHECK(handle.is_valid()); + DCHECK(!connector_); + connector_ = std::make_unique( + std::move(handle), mojo::Connector::SINGLE_THREADED_SEND, + base::ThreadTaskRunnerHandle::Get()); + connector_->PauseIncomingMethodCallProcessing(); + connector_->set_incoming_receiver(this); + connector_->set_connection_error_handler( + base::Bind(&MessagePort::Close, weak_factory_.GetWeakPtr())); + if (HasPendingActivity()) + Pin(); +} + +void MessagePort::Entangle(blink::MessagePortChannel channel) { + Entangle(channel.ReleaseHandle()); +} + +blink::MessagePortChannel MessagePort::Disentangle() { + DCHECK(!IsNeutered()); + auto result = blink::MessagePortChannel(connector_->PassMessagePipe()); + connector_ = nullptr; + if (!HasPendingActivity()) + Unpin(); + return result; +} + +bool MessagePort::HasPendingActivity() const { + // The spec says that entangled message ports should always be treated as if + // they have a strong reference. + // We'll also stipulate that the queue needs to be open (if the app drops its + // reference to the port before start()-ing it, then it's not really entangled + // as it's unreachable). + return started_ && IsEntangled(); +} + +// static +std::vector> MessagePort::EntanglePorts( + v8::Isolate* isolate, + std::vector channels) { + std::vector> wrapped_ports; + for (auto& port : channels) { + auto wrapped_port = MessagePort::Create(isolate); + wrapped_port->Entangle(std::move(port)); + wrapped_ports.emplace_back(wrapped_port); + } + return wrapped_ports; +} + +// static +std::vector MessagePort::DisentanglePorts( + v8::Isolate* isolate, + const std::vector>& ports, + bool* threw_exception) { + if (!ports.size()) + return std::vector(); + + std::unordered_set visited; + + // Walk the incoming array - if there are any duplicate ports, or null ports + // or cloned ports, throw an error (per section 8.3.3 of the HTML5 spec). + for (unsigned i = 0; i < ports.size(); ++i) { + auto* port = ports[i].get(); + if (!port || port->IsNeutered() || visited.find(port) != visited.end()) { + std::string type; + if (!port) + type = "null"; + else if (port->IsNeutered()) + type = "already neutered"; + else + type = "a duplicate"; + gin_helper::ErrorThrower(isolate).ThrowError( + "Port at index " + base::NumberToString(i) + " is " + type + "."); + *threw_exception = true; + return std::vector(); + } + visited.insert(port); + } + + // Passed-in ports passed validity checks, so we can disentangle them. + std::vector channels; + channels.reserve(ports.size()); + for (unsigned i = 0; i < ports.size(); ++i) + channels.push_back(ports[i]->Disentangle()); + return channels; +} + +void MessagePort::Pin() { + if (!pinned_.IsEmpty()) + return; + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + v8::Local self; + if (GetWrapper(isolate).ToLocal(&self)) { + pinned_.Reset(isolate, self); + } +} + +void MessagePort::Unpin() { + pinned_.Reset(); +} + +bool MessagePort::Accept(mojo::Message* mojo_message) { + blink::TransferableMessage message; + if (!blink::mojom::TransferableMessage::DeserializeFromMessage( + std::move(*mojo_message), &message)) { + return false; + } + + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + v8::HandleScope scope(isolate); + + auto ports = EntanglePorts(isolate, std::move(message.ports)); + + v8::Local message_value = DeserializeV8Value(isolate, message); + + v8::Local self; + if (!GetWrapper(isolate).ToLocal(&self)) + return false; + + auto event = gin::DataObjectBuilder(isolate) + .Set("data", message_value) + .Set("ports", ports) + .Build(); + gin_helper::EmitEvent(isolate, self, "message", event); + return true; +} + +gin::ObjectTemplateBuilder MessagePort::GetObjectTemplateBuilder( + v8::Isolate* isolate) { + return gin::Wrappable::GetObjectTemplateBuilder(isolate) + .SetMethod("postMessage", &MessagePort::PostMessage) + .SetMethod("start", &MessagePort::Start) + .SetMethod("close", &MessagePort::Close); +} + +const char* MessagePort::GetTypeName() { + return "MessagePort"; +} + +} // namespace electron + +namespace { + +using electron::MessagePort; + +v8::Local CreatePair(v8::Isolate* isolate) { + auto port1 = MessagePort::Create(isolate); + auto port2 = MessagePort::Create(isolate); + mojo::MessagePipe pipe; + port1->Entangle(std::move(pipe.handle0)); + port2->Entangle(std::move(pipe.handle1)); + return gin::DataObjectBuilder(isolate) + .Set("port1", port1) + .Set("port2", port2) + .Build(); +} + +void Initialize(v8::Local exports, + v8::Local unused, + v8::Local context, + void* priv) { + v8::Isolate* isolate = context->GetIsolate(); + gin_helper::Dictionary dict(isolate, exports); + dict.SetMethod("createPair", &CreatePair); +} + +} // namespace + +NODE_LINKED_MODULE_CONTEXT_AWARE(atom_browser_message_port, Initialize) diff --git a/shell/browser/api/message_port.h b/shell/browser/api/message_port.h new file mode 100644 index 0000000000000..fcd9e6309281b --- /dev/null +++ b/shell/browser/api/message_port.h @@ -0,0 +1,86 @@ +// Copyright (c) 2020 Slack Technologies, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef SHELL_BROWSER_API_MESSAGE_PORT_H_ +#define SHELL_BROWSER_API_MESSAGE_PORT_H_ + +#include +#include + +#include "gin/wrappable.h" +#include "mojo/public/cpp/bindings/connector.h" +#include "mojo/public/cpp/bindings/message.h" +#include "third_party/blink/public/common/messaging/message_port_channel.h" + +namespace gin { +class Arguments; +template +class Handle; +} // namespace gin + +namespace electron { + +// A non-blink version of blink::MessagePort. +class MessagePort : public gin::Wrappable, mojo::MessageReceiver { + public: + ~MessagePort() override; + static gin::Handle Create(v8::Isolate* isolate); + + void PostMessage(gin::Arguments* args); + void Start(); + void Close(); + + void Entangle(mojo::ScopedMessagePipeHandle handle); + void Entangle(blink::MessagePortChannel channel); + + blink::MessagePortChannel Disentangle(); + + bool IsEntangled() const { return !closed_ && !IsNeutered(); } + bool IsNeutered() const { return !connector_ || !connector_->is_valid(); } + + static std::vector> EntanglePorts( + v8::Isolate* isolate, + std::vector channels); + + static std::vector DisentanglePorts( + v8::Isolate* isolate, + const std::vector>& ports, + bool* threw_exception); + + // gin::Wrappable + gin::ObjectTemplateBuilder GetObjectTemplateBuilder( + v8::Isolate* isolate) override; + static gin::WrapperInfo kWrapperInfo; + const char* GetTypeName() override; + + private: + MessagePort(); + + // The blink version of MessagePort uses the very nice "ActiveScriptWrapper" + // class, which keeps the object alive through the V8 embedder hooks into the + // GC lifecycle: see + // https://source.chromium.org/chromium/chromium/src/+/master:third_party/blink/renderer/platform/heap/thread_state.cc;l=258;drc=b892cf58e162a8f66cd76d7472f129fe0fb6a7d1 + // We do not have that luxury, so we brutishly use v8::Global to accomplish + // something similar. Critically, whenever the value of + // "HasPendingActivity()" changes, we must call Pin() or Unpin() as + // appropriate. + bool HasPendingActivity() const; + void Pin(); + void Unpin(); + + // mojo::MessageReceiver + bool Accept(mojo::Message* mojo_message) override; + + std::unique_ptr connector_; + bool started_ = false; + bool closed_ = false; + + v8::Global pinned_; + + base::WeakPtrFactory weak_factory_{this}; +}; + +} // namespace electron + +#endif // SHELL_BROWSER_API_MESSAGE_PORT_H_ diff --git a/shell/common/api/api.mojom b/shell/common/api/api.mojom index ee580b2aadf34..8ff1efd2d14f1 100644 --- a/shell/common/api/api.mojom +++ b/shell/common/api/api.mojom @@ -3,6 +3,7 @@ module electron.mojom; import "mojo/public/mojom/base/string16.mojom"; import "ui/gfx/geometry/mojom/geometry.mojom"; import "third_party/blink/public/mojom/messaging/cloneable_message.mojom"; +import "third_party/blink/public/mojom/messaging/transferable_message.mojom"; interface ElectronRenderer { Message( @@ -12,6 +13,8 @@ interface ElectronRenderer { blink.mojom.CloneableMessage arguments, int32 sender_id); + ReceivePostMessage(string channel, blink.mojom.TransferableMessage message); + // This is an API specific to the "remote" module, and will ultimately be // replaced by generic IPC once WeakRef is generally available. [EnableIf=enable_remote_module] @@ -51,6 +54,8 @@ interface ElectronBrowser { string channel, blink.mojom.CloneableMessage arguments) => (blink.mojom.CloneableMessage result); + ReceivePostMessage(string channel, blink.mojom.TransferableMessage message); + // Emits an event on |channel| from the ipcMain JavaScript object in the main // process, and waits synchronously for a response. [Sync] diff --git a/shell/common/gin_converters/blink_converter.cc b/shell/common/gin_converters/blink_converter.cc index 0addfc36f507d..c0cec24552e60 100644 --- a/shell/common/gin_converters/blink_converter.cc +++ b/shell/common/gin_converters/blink_converter.cc @@ -16,6 +16,7 @@ #include "shell/common/gin_converters/value_converter.h" #include "shell/common/gin_helper/dictionary.h" #include "shell/common/keyboard_util.h" +#include "shell/common/v8_value_serializer.h" #include "third_party/blink/public/common/context_menu_data/edit_flags.h" #include "third_party/blink/public/common/input/web_input_event.h" #include "third_party/blink/public/common/input/web_keyboard_event.h" @@ -481,98 +482,16 @@ bool Converter::FromV8( return true; } -namespace { - -class V8Serializer : public v8::ValueSerializer::Delegate { - public: - explicit V8Serializer(v8::Isolate* isolate) - : isolate_(isolate), serializer_(isolate, this) {} - ~V8Serializer() override = default; - - bool Serialize(v8::Local value, blink::CloneableMessage* out) { - serializer_.WriteHeader(); - bool wrote_value; - if (!serializer_.WriteValue(isolate_->GetCurrentContext(), value) - .To(&wrote_value)) { - isolate_->ThrowException(v8::Exception::Error( - StringToV8(isolate_, "An object could not be cloned."))); - return false; - } - DCHECK(wrote_value); - - std::pair buffer = serializer_.Release(); - DCHECK_EQ(buffer.first, data_.data()); - out->encoded_message = base::make_span(buffer.first, buffer.second); - out->owned_encoded_message = std::move(data_); - - return true; - } - - // v8::ValueSerializer::Delegate - void* ReallocateBufferMemory(void* old_buffer, - size_t size, - size_t* actual_size) override { - DCHECK_EQ(old_buffer, data_.data()); - data_.resize(size); - *actual_size = data_.capacity(); - return data_.data(); - } - - void FreeBufferMemory(void* buffer) override { - DCHECK_EQ(buffer, data_.data()); - data_ = {}; - } - - void ThrowDataCloneError(v8::Local message) override { - isolate_->ThrowException(v8::Exception::Error(message)); - } - - private: - v8::Isolate* isolate_; - std::vector data_; - v8::ValueSerializer serializer_; -}; - -class V8Deserializer : public v8::ValueDeserializer::Delegate { - public: - V8Deserializer(v8::Isolate* isolate, const blink::CloneableMessage& message) - : isolate_(isolate), - deserializer_(isolate, - message.encoded_message.data(), - message.encoded_message.size(), - this) {} - - v8::Local Deserialize() { - v8::EscapableHandleScope scope(isolate_); - auto context = isolate_->GetCurrentContext(); - bool read_header; - if (!deserializer_.ReadHeader(context).To(&read_header)) - return v8::Null(isolate_); - DCHECK(read_header); - v8::Local value; - if (!deserializer_.ReadValue(context).ToLocal(&value)) { - return v8::Null(isolate_); - } - return scope.Escape(value); - } - - private: - v8::Isolate* isolate_; - v8::ValueDeserializer deserializer_; -}; - -} // namespace - v8::Local Converter::ToV8( v8::Isolate* isolate, const blink::CloneableMessage& in) { - return V8Deserializer(isolate, in).Deserialize(); + return electron::DeserializeV8Value(isolate, in); } bool Converter::FromV8(v8::Isolate* isolate, v8::Handle val, blink::CloneableMessage* out) { - return V8Serializer(isolate).Serialize(val, out); + return electron::SerializeV8Value(isolate, val, out); } } // namespace gin diff --git a/shell/common/gin_helper/callback.cc b/shell/common/gin_helper/callback.cc index f75276120b0ff..6258211bc3884 100644 --- a/shell/common/gin_helper/callback.cc +++ b/shell/common/gin_helper/callback.cc @@ -37,7 +37,7 @@ v8::Persistent g_call_translater; void CallTranslater(v8::Local external, v8::Local state, gin::Arguments* args) { - // Whether the callback should only be called for once. + // Whether the callback should only be called once. v8::Isolate* isolate = args->isolate(); auto context = isolate->GetCurrentContext(); bool one_time = @@ -47,7 +47,7 @@ void CallTranslater(v8::Local external, if (one_time) { auto called_symbol = gin::StringToSymbol(isolate, "called"); if (state->Has(context, called_symbol).ToChecked()) { - args->ThrowTypeError("callback can only be called for once"); + args->ThrowTypeError("One-time callback was called more than once"); return; } else { state->Set(context, called_symbol, v8::Boolean::New(isolate, true)) diff --git a/shell/common/gin_helper/function_template_extensions.h b/shell/common/gin_helper/function_template_extensions.h new file mode 100644 index 0000000000000..7b9588ab9fd19 --- /dev/null +++ b/shell/common/gin_helper/function_template_extensions.h @@ -0,0 +1,42 @@ +// Copyright 2020 Slack Technologies, Inc. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE.chromium file. + +#ifndef SHELL_COMMON_GIN_HELPER_FUNCTION_TEMPLATE_EXTENSIONS_H_ +#define SHELL_COMMON_GIN_HELPER_FUNCTION_TEMPLATE_EXTENSIONS_H_ + +#include + +#include "gin/function_template.h" +#include "shell/common/gin_helper/error_thrower.h" + +// This extends the functionality in //gin/function_template.h for "special" +// arguments to gin-bound methods. +// It's the counterpart to function_template.h, which includes these methods +// in the gin_helper namespace. +namespace gin { + +// Support base::Optional as an argument. +template +bool GetNextArgument(Arguments* args, + const InvokerOptions& invoker_options, + bool is_first, + base::Optional* result) { + T converted; + // Use gin::Arguments::GetNext which always advances |next| counter. + if (args->GetNext(&converted)) + result->emplace(std::move(converted)); + return true; +} + +inline bool GetNextArgument(Arguments* args, + const InvokerOptions& invoker_options, + bool is_first, + gin_helper::ErrorThrower* result) { + *result = gin_helper::ErrorThrower(args->isolate()); + return true; +} + +} // namespace gin + +#endif // SHELL_COMMON_GIN_HELPER_FUNCTION_TEMPLATE_EXTENSIONS_H_ diff --git a/shell/common/node_bindings.cc b/shell/common/node_bindings.cc index f35e15d920695..32daef14d013c 100644 --- a/shell/common/node_bindings.cc +++ b/shell/common/node_bindings.cc @@ -45,6 +45,7 @@ V(atom_browser_global_shortcut) \ V(atom_browser_in_app_purchase) \ V(atom_browser_menu) \ + V(atom_browser_message_port) \ V(atom_browser_net) \ V(atom_browser_power_monitor) \ V(atom_browser_power_save_blocker) \ diff --git a/shell/common/v8_value_serializer.cc b/shell/common/v8_value_serializer.cc new file mode 100644 index 0000000000000..73726976203c3 --- /dev/null +++ b/shell/common/v8_value_serializer.cc @@ -0,0 +1,147 @@ +// Copyright (c) 2020 Slack Technologies, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "shell/common/v8_value_serializer.h" + +#include +#include + +#include "gin/converter.h" +#include "third_party/blink/public/common/messaging/cloneable_message.h" +#include "v8/include/v8.h" + +namespace electron { + +namespace { +const uint8_t kVersionTag = 0xFF; +} // namespace + +class V8Serializer : public v8::ValueSerializer::Delegate { + public: + explicit V8Serializer(v8::Isolate* isolate) + : isolate_(isolate), serializer_(isolate, this) {} + ~V8Serializer() override = default; + + bool Serialize(v8::Local value, blink::CloneableMessage* out) { + WriteBlinkEnvelope(19); + + serializer_.WriteHeader(); + bool wrote_value; + if (!serializer_.WriteValue(isolate_->GetCurrentContext(), value) + .To(&wrote_value)) { + isolate_->ThrowException(v8::Exception::Error( + gin::StringToV8(isolate_, "An object could not be cloned."))); + return false; + } + DCHECK(wrote_value); + + std::pair buffer = serializer_.Release(); + DCHECK_EQ(buffer.first, data_.data()); + out->encoded_message = base::make_span(buffer.first, buffer.second); + out->owned_encoded_message = std::move(data_); + + return true; + } + + // v8::ValueSerializer::Delegate + void* ReallocateBufferMemory(void* old_buffer, + size_t size, + size_t* actual_size) override { + DCHECK_EQ(old_buffer, data_.data()); + data_.resize(size); + *actual_size = data_.capacity(); + return data_.data(); + } + + void FreeBufferMemory(void* buffer) override { + DCHECK_EQ(buffer, data_.data()); + data_ = {}; + } + + void ThrowDataCloneError(v8::Local message) override { + isolate_->ThrowException(v8::Exception::Error(message)); + } + + private: + void WriteTag(uint8_t tag) { serializer_.WriteRawBytes(&tag, 1); } + + void WriteBlinkEnvelope(uint32_t blink_version) { + // Write a dummy blink version envelope for compatibility with + // blink::V8ScriptValueSerializer + WriteTag(kVersionTag); + serializer_.WriteUint32(blink_version); + } + + v8::Isolate* isolate_; + std::vector data_; + v8::ValueSerializer serializer_; +}; + +class V8Deserializer : public v8::ValueDeserializer::Delegate { + public: + V8Deserializer(v8::Isolate* isolate, base::span data) + : isolate_(isolate), + deserializer_(isolate, data.data(), data.size(), this) {} + V8Deserializer(v8::Isolate* isolate, const blink::CloneableMessage& message) + : V8Deserializer(isolate, message.encoded_message) {} + + v8::Local Deserialize() { + v8::EscapableHandleScope scope(isolate_); + auto context = isolate_->GetCurrentContext(); + + uint32_t blink_version; + if (!ReadBlinkEnvelope(&blink_version)) + return v8::Null(isolate_); + + bool read_header; + if (!deserializer_.ReadHeader(context).To(&read_header)) + return v8::Null(isolate_); + DCHECK(read_header); + v8::Local value; + if (!deserializer_.ReadValue(context).ToLocal(&value)) + return v8::Null(isolate_); + return scope.Escape(value); + } + + private: + bool ReadTag(uint8_t* tag) { + const void* tag_bytes = nullptr; + if (!deserializer_.ReadRawBytes(1, &tag_bytes)) + return false; + *tag = *reinterpret_cast(tag_bytes); + return true; + } + + bool ReadBlinkEnvelope(uint32_t* blink_version) { + // Read a dummy blink version envelope for compatibility with + // blink::V8ScriptValueDeserializer + uint8_t tag = 0; + if (!ReadTag(&tag) || tag != kVersionTag) + return false; + if (!deserializer_.ReadUint32(blink_version)) + return false; + return true; + } + + v8::Isolate* isolate_; + v8::ValueDeserializer deserializer_; +}; + +bool SerializeV8Value(v8::Isolate* isolate, + v8::Local value, + blink::CloneableMessage* out) { + return V8Serializer(isolate).Serialize(value, out); +} + +v8::Local DeserializeV8Value(v8::Isolate* isolate, + const blink::CloneableMessage& in) { + return V8Deserializer(isolate, in).Deserialize(); +} + +v8::Local DeserializeV8Value(v8::Isolate* isolate, + base::span data) { + return V8Deserializer(isolate, data).Deserialize(); +} + +} // namespace electron diff --git a/shell/common/v8_value_serializer.h b/shell/common/v8_value_serializer.h new file mode 100644 index 0000000000000..1a2513a6497be --- /dev/null +++ b/shell/common/v8_value_serializer.h @@ -0,0 +1,33 @@ +// Copyright (c) 2020 Slack Technologies, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef SHELL_COMMON_V8_VALUE_SERIALIZER_H_ +#define SHELL_COMMON_V8_VALUE_SERIALIZER_H_ + +#include "base/containers/span.h" + +namespace v8 { +class Isolate; +template +class Local; +class Value; +} // namespace v8 + +namespace blink { +struct CloneableMessage; +} + +namespace electron { + +bool SerializeV8Value(v8::Isolate* isolate, + v8::Local value, + blink::CloneableMessage* out); +v8::Local DeserializeV8Value(v8::Isolate* isolate, + const blink::CloneableMessage& in); +v8::Local DeserializeV8Value(v8::Isolate* isolate, + base::span data); + +} // namespace electron + +#endif // SHELL_COMMON_V8_VALUE_SERIALIZER_H_ diff --git a/shell/renderer/api/electron_api_renderer_ipc.cc b/shell/renderer/api/electron_api_renderer_ipc.cc index 311b824853552..fb69f6a6673ab 100644 --- a/shell/renderer/api/electron_api_renderer_ipc.cc +++ b/shell/renderer/api/electron_api_renderer_ipc.cc @@ -17,10 +17,13 @@ #include "shell/common/gin_converters/blink_converter.h" #include "shell/common/gin_converters/value_converter.h" #include "shell/common/gin_helper/error_thrower.h" +#include "shell/common/gin_helper/function_template_extensions.h" #include "shell/common/gin_helper/promise.h" #include "shell/common/node_bindings.h" #include "shell/common/node_includes.h" +#include "shell/common/v8_value_serializer.h" #include "third_party/blink/public/web/web_local_frame.h" +#include "third_party/blink/public/web/web_message_port_converter.h" using blink::WebLocalFrame; using content::RenderFrame; @@ -76,7 +79,8 @@ class IPCRenderer : public gin::Wrappable, .SetMethod("sendSync", &IPCRenderer::SendSync) .SetMethod("sendTo", &IPCRenderer::SendTo) .SetMethod("sendToHost", &IPCRenderer::SendToHost) - .SetMethod("invoke", &IPCRenderer::Invoke); + .SetMethod("invoke", &IPCRenderer::Invoke) + .SetMethod("postMessage", &IPCRenderer::PostMessage); } const char* GetTypeName() override { return "IPCRenderer"; } @@ -92,7 +96,7 @@ class IPCRenderer : public gin::Wrappable, return; } blink::CloneableMessage message; - if (!gin::ConvertFromV8(isolate, arguments, &message)) { + if (!electron::SerializeV8Value(isolate, arguments, &message)) { return; } electron_browser_ptr_->Message(internal, channel, std::move(message)); @@ -108,7 +112,7 @@ class IPCRenderer : public gin::Wrappable, return v8::Local(); } blink::CloneableMessage message; - if (!gin::ConvertFromV8(isolate, arguments, &message)) { + if (!electron::SerializeV8Value(isolate, arguments, &message)) { return v8::Local(); } gin_helper::Promise p(isolate); @@ -124,6 +128,43 @@ class IPCRenderer : public gin::Wrappable, return handle; } + void PostMessage(v8::Isolate* isolate, + gin_helper::ErrorThrower thrower, + const std::string& channel, + v8::Local message_value, + base::Optional> transfer) { + blink::TransferableMessage transferable_message; + if (!electron::SerializeV8Value(isolate, message_value, + &transferable_message)) { + // SerializeV8Value sets an exception. + return; + } + + std::vector> transferables; + if (transfer) { + if (!gin::ConvertFromV8(isolate, *transfer, &transferables)) { + thrower.ThrowTypeError("Invalid value for transfer"); + return; + } + } + + std::vector ports; + for (auto& transferable : transferables) { + base::Optional port = + blink::WebMessagePortConverter:: + DisentangleAndExtractMessagePortChannel(isolate, transferable); + if (!port.has_value()) { + thrower.ThrowTypeError("Invalid value for transfer"); + return; + } + ports.emplace_back(port.value()); + } + + transferable_message.ports = std::move(ports); + electron_browser_ptr_->ReceivePostMessage(channel, + std::move(transferable_message)); + } + void SendTo(v8::Isolate* isolate, bool internal, bool send_to_all, @@ -136,7 +177,7 @@ class IPCRenderer : public gin::Wrappable, return; } blink::CloneableMessage message; - if (!gin::ConvertFromV8(isolate, arguments, &message)) { + if (!electron::SerializeV8Value(isolate, arguments, &message)) { return; } electron_browser_ptr_->MessageTo(internal, send_to_all, web_contents_id, @@ -152,30 +193,30 @@ class IPCRenderer : public gin::Wrappable, return; } blink::CloneableMessage message; - if (!gin::ConvertFromV8(isolate, arguments, &message)) { + if (!electron::SerializeV8Value(isolate, arguments, &message)) { return; } electron_browser_ptr_->MessageHost(channel, std::move(message)); } - blink::CloneableMessage SendSync(v8::Isolate* isolate, - bool internal, - const std::string& channel, - v8::Local arguments) { + v8::Local SendSync(v8::Isolate* isolate, + bool internal, + const std::string& channel, + v8::Local arguments) { if (!electron_browser_ptr_) { gin_helper::ErrorThrower(isolate).ThrowError( kIPCMethodCalledAfterContextReleasedError); - return blink::CloneableMessage(); + return v8::Local(); } blink::CloneableMessage message; - if (!gin::ConvertFromV8(isolate, arguments, &message)) { - return blink::CloneableMessage(); + if (!electron::SerializeV8Value(isolate, arguments, &message)) { + return v8::Local(); } blink::CloneableMessage result; electron_browser_ptr_->MessageSync(internal, channel, std::move(message), &result); - return result; + return electron::DeserializeV8Value(isolate, result); } v8::Global weak_context_; diff --git a/shell/renderer/electron_api_service_impl.cc b/shell/renderer/electron_api_service_impl.cc index fb102b4844268..5999dd9c0bc8a 100644 --- a/shell/renderer/electron_api_service_impl.cc +++ b/shell/renderer/electron_api_service_impl.cc @@ -11,6 +11,7 @@ #include "base/environment.h" #include "base/macros.h" #include "base/threading/thread_restrictions.h" +#include "gin/data_object_builder.h" #include "mojo/public/cpp/system/platform_handle.h" #include "shell/common/electron_constants.h" #include "shell/common/gin_converters/blink_converter.h" @@ -18,10 +19,12 @@ #include "shell/common/heap_snapshot.h" #include "shell/common/node_includes.h" #include "shell/common/options_switches.h" +#include "shell/common/v8_value_serializer.h" #include "shell/renderer/electron_render_frame_observer.h" #include "shell/renderer/renderer_client_base.h" #include "third_party/blink/public/web/blink.h" #include "third_party/blink/public/web/web_local_frame.h" +#include "third_party/blink/public/web/web_message_port_converter.h" namespace electron { @@ -74,6 +77,7 @@ void InvokeIpcCallback(v8::Local context, void EmitIPCEvent(v8::Local context, bool internal, const std::string& channel, + std::vector> ports, v8::Local args, int32_t sender_id) { auto* isolate = context->GetIsolate(); @@ -85,7 +89,8 @@ void EmitIPCEvent(v8::Local context, std::vector> argv = { gin::ConvertToV8(isolate, internal), gin::ConvertToV8(isolate, channel), - args, gin::ConvertToV8(isolate, sender_id)}; + gin::ConvertToV8(isolate, ports), args, + gin::ConvertToV8(isolate, sender_id)}; InvokeIpcCallback(context, "onMessage", argv); } @@ -161,7 +166,7 @@ void ElectronApiServiceImpl::Message(bool internal, v8::Local args = gin::ConvertToV8(isolate, arguments); - EmitIPCEvent(context, internal, channel, args, sender_id); + EmitIPCEvent(context, internal, channel, {}, args, sender_id); // Also send the message to all sub-frames. // TODO(MarshallOfSound): Completely move this logic to the main process @@ -171,11 +176,39 @@ void ElectronApiServiceImpl::Message(bool internal, if (child->IsWebLocalFrame()) { v8::Local child_context = renderer_client_->GetContext(child->ToWebLocalFrame(), isolate); - EmitIPCEvent(child_context, internal, channel, args, sender_id); + EmitIPCEvent(child_context, internal, channel, {}, args, sender_id); } } } +void ElectronApiServiceImpl::ReceivePostMessage( + const std::string& channel, + blink::TransferableMessage message) { + blink::WebLocalFrame* frame = render_frame()->GetWebFrame(); + if (!frame) + return; + + v8::Isolate* isolate = blink::MainThreadIsolate(); + v8::HandleScope handle_scope(isolate); + + v8::Local context = renderer_client_->GetContext(frame, isolate); + v8::Context::Scope context_scope(context); + + v8::Local message_value = DeserializeV8Value(isolate, message); + + std::vector> ports; + for (auto& port : message.ports) { + ports.emplace_back( + blink::WebMessagePortConverter::EntangleAndInjectMessagePortChannel( + context, std::move(port))); + } + + std::vector> args = {message_value}; + + EmitIPCEvent(context, false, channel, ports, gin::ConvertToV8(isolate, args), + 0); +} + #if BUILDFLAG(ENABLE_REMOTE_MODULE) void ElectronApiServiceImpl::DereferenceRemoteJSCallback( const std::string& context_id, @@ -198,7 +231,7 @@ void ElectronApiServiceImpl::DereferenceRemoteJSCallback( args.AppendInteger(object_id); v8::Local v8_args = gin::ConvertToV8(isolate, args); - EmitIPCEvent(context, true /* internal */, channel, v8_args, + EmitIPCEvent(context, true /* internal */, channel, {}, v8_args, 0 /* sender_id */); } #endif diff --git a/shell/renderer/electron_api_service_impl.h b/shell/renderer/electron_api_service_impl.h index 570fed471c42e..1b48d10bbf36e 100644 --- a/shell/renderer/electron_api_service_impl.h +++ b/shell/renderer/electron_api_service_impl.h @@ -33,6 +33,8 @@ class ElectronApiServiceImpl : public mojom::ElectronRenderer, const std::string& channel, blink::CloneableMessage arguments, int32_t sender_id) override; + void ReceivePostMessage(const std::string& channel, + blink::TransferableMessage message) override; #if BUILDFLAG(ENABLE_REMOTE_MODULE) void DereferenceRemoteJSCallback(const std::string& context_id, int32_t object_id) override; diff --git a/spec-main/api-ipc-spec.ts b/spec-main/api-ipc-spec.ts index 664af04e08afa..5b3c9293d2956 100644 --- a/spec-main/api-ipc-spec.ts +++ b/spec-main/api-ipc-spec.ts @@ -1,5 +1,10 @@ +import { EventEmitter } from 'events'; import { expect } from 'chai'; -import { BrowserWindow, ipcMain, IpcMainInvokeEvent } from 'electron'; +import { BrowserWindow, ipcMain, IpcMainInvokeEvent, MessageChannelMain } from 'electron'; +import { closeAllWindows } from './window-helpers'; +import { emittedOnce } from './events-helpers'; + +const v8Util = process.electronBinding('v8_util'); describe('ipc module', () => { describe('invoke', () => { @@ -181,4 +186,298 @@ describe('ipc module', () => { expect(received).to.deep.equal([...received].sort((a, b) => a - b)); }); }); + + describe('MessagePort', () => { + afterEach(closeAllWindows); + + it('can send a port to the main process', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }); + w.loadURL('about:blank'); + const p = emittedOnce(ipcMain, 'port'); + await w.webContents.executeJavaScript(`(${function () { + const channel = new MessageChannel(); + require('electron').ipcRenderer.postMessage('port', 'hi', [channel.port1]); + }})()`); + const [ev, msg] = await p; + expect(msg).to.equal('hi'); + expect(ev.ports).to.have.length(1); + const [port] = ev.ports; + expect(port).to.be.an.instanceOf(EventEmitter); + }); + + it('can communicate between main and renderer', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }); + w.loadURL('about:blank'); + const p = emittedOnce(ipcMain, 'port'); + await w.webContents.executeJavaScript(`(${function () { + const channel = new MessageChannel(); + (channel.port2 as any).onmessage = (ev: any) => { + channel.port2.postMessage(ev.data * 2); + }; + require('electron').ipcRenderer.postMessage('port', '', [channel.port1]); + }})()`); + const [ev] = await p; + expect(ev.ports).to.have.length(1); + const [port] = ev.ports; + port.start(); + port.postMessage(42); + const [ev2] = await emittedOnce(port, 'message'); + expect(ev2.data).to.equal(84); + }); + + it('can receive a port from a renderer over a MessagePort connection', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }); + w.loadURL('about:blank'); + function fn () { + const channel1 = new MessageChannel(); + const channel2 = new MessageChannel(); + channel1.port2.postMessage('', [channel2.port1]); + channel2.port2.postMessage('matryoshka'); + require('electron').ipcRenderer.postMessage('port', '', [channel1.port1]); + } + w.webContents.executeJavaScript(`(${fn})()`); + const [{ ports: [port1] }] = await emittedOnce(ipcMain, 'port'); + port1.start(); + const [{ ports: [port2] }] = await emittedOnce(port1, 'message'); + port2.start(); + const [{ data }] = await emittedOnce(port2, 'message'); + expect(data).to.equal('matryoshka'); + }); + + it('can forward a port from one renderer to another renderer', async () => { + const w1 = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }); + const w2 = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }); + w1.loadURL('about:blank'); + w2.loadURL('about:blank'); + w1.webContents.executeJavaScript(`(${function () { + const channel = new MessageChannel(); + (channel.port2 as any).onmessage = (ev: any) => { + require('electron').ipcRenderer.send('message received', ev.data); + }; + require('electron').ipcRenderer.postMessage('port', '', [channel.port1]); + }})()`); + const [{ ports: [port] }] = await emittedOnce(ipcMain, 'port'); + await w2.webContents.executeJavaScript(`(${function () { + require('electron').ipcRenderer.on('port', ({ ports: [port] }: any) => { + port.postMessage('a message'); + }); + }})()`); + w2.webContents.postMessage('port', '', [port]); + const [, data] = await emittedOnce(ipcMain, 'message received'); + expect(data).to.equal('a message'); + }); + + describe('MessageChannelMain', () => { + it('can be created', () => { + const { port1, port2 } = new MessageChannelMain(); + expect(port1).not.to.be.null(); + expect(port2).not.to.be.null(); + }); + + it('can send messages within the process', async () => { + const { port1, port2 } = new MessageChannelMain(); + port2.postMessage('hello'); + port1.start(); + const [ev] = await emittedOnce(port1, 'message'); + expect(ev.data).to.equal('hello'); + }); + + it('can pass one end to a WebContents', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }); + w.loadURL('about:blank'); + await w.webContents.executeJavaScript(`(${function () { + const { ipcRenderer } = require('electron'); + ipcRenderer.on('port', (ev) => { + const [port] = ev.ports; + port.onmessage = () => { + ipcRenderer.send('done'); + }; + }); + }})()`); + const { port1, port2 } = new MessageChannelMain(); + port1.postMessage('hello'); + w.webContents.postMessage('port', null, [port2]); + await emittedOnce(ipcMain, 'done'); + }); + + it('can be passed over another channel', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }); + w.loadURL('about:blank'); + await w.webContents.executeJavaScript(`(${function () { + const { ipcRenderer } = require('electron'); + ipcRenderer.on('port', (e1) => { + e1.ports[0].onmessage = (e2) => { + e2.ports[0].onmessage = (e3) => { + ipcRenderer.send('done', e3.data); + }; + }; + }); + }})()`); + const { port1, port2 } = new MessageChannelMain(); + const { port1: port3, port2: port4 } = new MessageChannelMain(); + port1.postMessage(null, [port4]); + port3.postMessage('hello'); + w.webContents.postMessage('port', null, [port2]); + const [, message] = await emittedOnce(ipcMain, 'done'); + expect(message).to.equal('hello'); + }); + + it('can send messages to a closed port', () => { + const { port1, port2 } = new MessageChannelMain(); + port2.start(); + port2.on('message', () => { throw new Error('unexpected message received'); }); + port1.close(); + port1.postMessage('hello'); + }); + + it('can send messages to a port whose remote end is closed', () => { + const { port1, port2 } = new MessageChannelMain(); + port2.start(); + port2.on('message', () => { throw new Error('unexpected message received'); }); + port2.close(); + port1.postMessage('hello'); + }); + + it('throws when passing null ports', () => { + const { port1 } = new MessageChannelMain(); + expect(() => { + port1.postMessage(null, [null] as any); + }).to.throw(/conversion failure/); + }); + + it('throws when passing duplicate ports', () => { + const { port1 } = new MessageChannelMain(); + const { port1: port3 } = new MessageChannelMain(); + expect(() => { + port1.postMessage(null, [port3, port3]); + }).to.throw(/duplicate/); + }); + + it('throws when passing ports that have already been neutered', () => { + const { port1 } = new MessageChannelMain(); + const { port1: port3 } = new MessageChannelMain(); + port1.postMessage(null, [port3]); + expect(() => { + port1.postMessage(null, [port3]); + }).to.throw(/already neutered/); + }); + + it('throws when passing itself', () => { + const { port1 } = new MessageChannelMain(); + expect(() => { + port1.postMessage(null, [port1]); + }).to.throw(/contains the source port/); + }); + + describe('GC behavior', () => { + it('is not collected while it could still receive messages', async () => { + let trigger: Function; + const promise = new Promise(resolve => { trigger = resolve; }); + const port1 = (() => { + const { port1, port2 } = new MessageChannelMain(); + + port2.on('message', (e) => { trigger(e.data); }); + port2.start(); + return port1; + })(); + v8Util.requestGarbageCollectionForTesting(); + port1.postMessage('hello'); + expect(await promise).to.equal('hello'); + }); + }); + }); + + describe('WebContents.postMessage', () => { + it('sends a message', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }); + w.loadURL('about:blank'); + await w.webContents.executeJavaScript(`(${function () { + const { ipcRenderer } = require('electron'); + ipcRenderer.on('foo', (e, msg) => { + ipcRenderer.send('bar', msg); + }); + }})()`); + w.webContents.postMessage('foo', { some: 'message' }); + const [, msg] = await emittedOnce(ipcMain, 'bar'); + expect(msg).to.deep.equal({ some: 'message' }); + }); + + describe('error handling', () => { + it('throws on missing channel', async () => { + const w = new BrowserWindow({ show: false }); + await w.loadURL('about:blank'); + expect(() => { + (w.webContents.postMessage as any)(); + }).to.throw(/Insufficient number of arguments/); + }); + + it('throws on invalid channel', async () => { + const w = new BrowserWindow({ show: false }); + await w.loadURL('about:blank'); + expect(() => { + w.webContents.postMessage(null as any, '', []); + }).to.throw(/Error processing argument at index 0/); + }); + + it('throws on missing message', async () => { + const w = new BrowserWindow({ show: false }); + await w.loadURL('about:blank'); + expect(() => { + (w.webContents.postMessage as any)('channel'); + }).to.throw(/Insufficient number of arguments/); + }); + + it('throws on non-serializable message', async () => { + const w = new BrowserWindow({ show: false }); + await w.loadURL('about:blank'); + expect(() => { + w.webContents.postMessage('channel', w); + }).to.throw(/An object could not be cloned/); + }); + + it('throws on invalid transferable list', async () => { + const w = new BrowserWindow({ show: false }); + await w.loadURL('about:blank'); + expect(() => { + w.webContents.postMessage('', '', null as any); + }).to.throw(/Invalid value for transfer/); + }); + + it('throws on transferring non-transferable', async () => { + const w = new BrowserWindow({ show: false }); + await w.loadURL('about:blank'); + expect(() => { + (w.webContents.postMessage as any)('channel', '', [123]); + }).to.throw(/Invalid value for transfer/); + }); + + it('throws when passing null ports', async () => { + const w = new BrowserWindow({ show: false }); + await w.loadURL('about:blank'); + expect(() => { + w.webContents.postMessage('foo', null, [null] as any); + }).to.throw(/Invalid value for transfer/); + }); + + it('throws when passing duplicate ports', async () => { + const w = new BrowserWindow({ show: false }); + await w.loadURL('about:blank'); + const { port1 } = new MessageChannelMain(); + expect(() => { + w.webContents.postMessage('foo', null, [port1, port1]); + }).to.throw(/duplicate/); + }); + + it('throws when passing ports that have already been neutered', async () => { + const w = new BrowserWindow({ show: false }); + await w.loadURL('about:blank'); + const { port1 } = new MessageChannelMain(); + w.webContents.postMessage('foo', null, [port1]); + expect(() => { + w.webContents.postMessage('foo', null, [port1]); + }).to.throw(/already neutered/); + }); + }); + }); + }); }); diff --git a/typings/internal-ambient.d.ts b/typings/internal-ambient.d.ts index e32d426e4743a..43dae70d60698 100644 --- a/typings/internal-ambient.d.ts +++ b/typings/internal-ambient.d.ts @@ -24,12 +24,13 @@ declare namespace NodeJS { isComponentBuild(): boolean; } - interface IpcBinding { + interface IpcRendererBinding { send(internal: boolean, channel: string, args: any[]): void; sendSync(internal: boolean, channel: string, args: any[]): any; sendToHost(channel: string, args: any[]): void; sendTo(internal: boolean, sendToAll: boolean, webContentsId: number, channel: string, args: any[]): void; invoke(internal: boolean, channel: string, args: any[]): Promise<{ error: string, result: T }>; + postMessage(channel: string, message: any, transferables: MessagePort[]): void; } interface V8UtilBinding { @@ -52,7 +53,7 @@ declare namespace NodeJS { _linkedBinding(name: string): any; electronBinding(name: string): any; electronBinding(name: 'features'): FeaturesBinding; - electronBinding(name: 'ipc'): { ipc: IpcBinding }; + electronBinding(name: 'ipc'): { ipc: IpcRendererBinding }; electronBinding(name: 'v8_util'): V8UtilBinding; electronBinding(name: 'app'): { app: Electron.App, App: Function }; electronBinding(name: 'command_line'): Electron.CommandLine;