feat: service worker IPC

This commit is contained in:
Samuel Maddock
2024-10-28 18:57:04 -04:00
parent f59d8d618a
commit 0534f9f187
32 changed files with 1082 additions and 164 deletions

View File

@@ -0,0 +1,68 @@
## Class: IpcMainServiceWorker
> Communicate asynchronously from the main process to service workers.
Process: [Main](../glossary.md#main-process)
### Instance Methods
#### `ipcMainServiceWorker.on(channel, listener)`
* `channel` string
* `listener` Function
* `event` [IpcMainServiceWorkerEvent][ipc-main-service-worker-event]
* `...args` any[]
Listens to `channel`, when a new message arrives `listener` would be called with
`listener(event, args...)`.
#### `ipcMainServiceWorker.once(channel, listener)`
* `channel` string
* `listener` Function
* `event` [IpcMainServiceWorkerEvent][ipc-main-service-worker-event]
* `...args` any[]
Adds a one time `listener` function for the event. This `listener` is invoked
only the next time a message is sent to `channel`, after which it is removed.
#### `ipcMainServiceWorker.removeListener(channel, listener)`
* `channel` string
* `listener` Function
* `...args` any[]
Removes the specified `listener` from the listener array for the specified
`channel`.
#### `ipcMainServiceWorker.removeAllListeners([channel])`
* `channel` string (optional)
Removes listeners of the specified `channel`.
#### `ipcMainServiceWorker.handle(channel, listener)`
* `channel` string
* `listener` Function\<Promise\<any\> | any\>
* `event` [IpcMainServiceWorkerInvokeEvent][ipc-main-service-worker-invoke-event]
* `...args` any[]
#### `ipcMainServiceWorker.handleOnce(channel, listener)`
* `channel` string
* `listener` Function\<Promise\<any\> | any\>
* `event` [IpcMainServiceWorkerInvokeEvent][ipc-main-service-worker-invoke-event]
* `...args` any[]
Handles a single `invoke`able IPC message, then removes the listener. See
`ipcMainServiceWorker.handle(channel, listener)`.
#### `ipcMainServiceWorker.removeHandler(channel)`
* `channel` string
Removes any handler for `channel`, if present.
[ipc-main-service-worker-event]:../api/structures/ipc-main-service-worker-event.md
[ipc-main-service-worker-invoke-event]:../api/structures/ipc-main-service-worker-invoke-event.md

View File

@@ -1,5 +1,6 @@
# IpcMainEvent Object extends `Event`
* `type` String - Possible values include `frame`
* `processId` Integer - The internal ID of the renderer process that sent this message
* `frameId` Integer - The ID of the renderer frame that sent this message
* `returnValue` any - Set this to the value to be returned in a synchronous message

View File

@@ -1,5 +1,6 @@
# IpcMainInvokeEvent Object extends `Event`
* `type` String - Possible values include `frame`
* `processId` Integer - The internal ID of the renderer process that sent this message
* `frameId` Integer - The ID of the renderer frame that sent this message
* `sender` [WebContents](../web-contents.md) - Returns the `webContents` that sent the message

View File

@@ -0,0 +1,11 @@
# IpcMainServiceWorkerEvent Object extends `Event`
* `type` String - Possible values include `service-worker`.
* `serviceWorker` [ServiceWorkerMain](../service-worker-main.md) _Readonly_ - The service worker that sent this message
* `versionId` Number - The service worker version ID.
* `session` Session - The [`Session`](../session.md) instance with which the event is associated.
* `returnValue` any - Set this to the value to be returned in a synchronous message
* `ports` [MessagePortMain](../message-port-main.md)[] - A list of MessagePorts that were transferred with this message
* `reply` Function - A function that will send an IPC message to the renderer frame that sent the original message that you are currently handling. You should use this method to "reply" to the sent message in order to guarantee the reply will go to the correct process and frame.
* `channel` string
* `...args` any[]

View File

@@ -0,0 +1,6 @@
# IpcMainServiceWorkerInvokeEvent Object extends `Event`
* `type` String - Possible values include `service-worker`.
* `serviceWorker` [ServiceWorkerMain](../service-worker-main.md) _Readonly_ - The service worker that sent this message
* `versionId` Number - The service worker version ID.
* `session` Session - The [`Session`](../session.md) instance with which the event is associated.

View File

@@ -25,6 +25,7 @@ auto_filenames = {
"docs/api/global-shortcut.md",
"docs/api/in-app-purchase.md",
"docs/api/incoming-message.md",
"docs/api/ipc-main-service-worker.md",
"docs/api/ipc-main.md",
"docs/api/ipc-renderer.md",
"docs/api/menu-item.md",
@@ -45,7 +46,7 @@ auto_filenames = {
"docs/api/push-notifications.md",
"docs/api/safe-storage.md",
"docs/api/screen.md",
"docs/api/service-worker-main.md",
"docs/api/service-worker-main.md",
"docs/api/service-workers.md",
"docs/api/session.md",
"docs/api/share-menu.md",
@@ -95,6 +96,8 @@ auto_filenames = {
"docs/api/structures/input-event.md",
"docs/api/structures/ipc-main-event.md",
"docs/api/structures/ipc-main-invoke-event.md",
"docs/api/structures/ipc-main-service-worker-event.md",
"docs/api/structures/ipc-main-service-worker-invoke-event.md",
"docs/api/structures/ipc-renderer-event.md",
"docs/api/structures/jump-list-category.md",
"docs/api/structures/jump-list-item.md",
@@ -172,6 +175,7 @@ auto_filenames = {
"lib/renderer/api/web-utils.ts",
"lib/renderer/common-init.ts",
"lib/renderer/inspector.ts",
"lib/renderer/ipc-native-setup.ts",
"lib/renderer/ipc-renderer-internal-utils.ts",
"lib/renderer/ipc-renderer-internal.ts",
"lib/renderer/security-warnings.ts",
@@ -260,6 +264,7 @@ auto_filenames = {
"lib/browser/guest-view-manager.ts",
"lib/browser/guest-window-manager.ts",
"lib/browser/init.ts",
"lib/browser/ipc-dispatch.ts",
"lib/browser/ipc-main-impl.ts",
"lib/browser/ipc-main-internal-utils.ts",
"lib/browser/ipc-main-internal.ts",
@@ -304,6 +309,7 @@ auto_filenames = {
"lib/renderer/common-init.ts",
"lib/renderer/init.ts",
"lib/renderer/inspector.ts",
"lib/renderer/ipc-native-setup.ts",
"lib/renderer/ipc-renderer-internal-utils.ts",
"lib/renderer/ipc-renderer-internal.ts",
"lib/renderer/security-warnings.ts",

View File

@@ -304,7 +304,7 @@ filenames = {
"shell/browser/api/electron_api_screen.h",
"shell/browser/api/electron_api_service_worker_context.cc",
"shell/browser/api/electron_api_service_worker_context.h",
"shell/browser/api/electron_api_service_worker_main.cc",
"shell/browser/api/electron_api_service_worker_main.cc",
"shell/browser/api/electron_api_service_worker_main.h",
"shell/browser/api/electron_api_session.cc",
"shell/browser/api/electron_api_session.h",
@@ -332,6 +332,7 @@ filenames = {
"shell/browser/api/gpu_info_enumerator.h",
"shell/browser/api/gpuinfo_manager.cc",
"shell/browser/api/gpuinfo_manager.h",
"shell/browser/api/ipc_dispatcher.h",
"shell/browser/api/message_port.cc",
"shell/browser/api/message_port.h",
"shell/browser/api/process_metric.cc",
@@ -363,6 +364,8 @@ filenames = {
"shell/browser/draggable_region_provider.h",
"shell/browser/electron_api_ipc_handler_impl.cc",
"shell/browser/electron_api_ipc_handler_impl.h",
"shell/browser/electron_api_sw_ipc_handler_impl.cc",
"shell/browser/electron_api_sw_ipc_handler_impl.h",
"shell/browser/electron_autofill_driver.cc",
"shell/browser/electron_autofill_driver.h",
"shell/browser/electron_autofill_driver_factory.cc",
@@ -624,7 +627,7 @@ filenames = {
"shell/common/gin_converters/osr_converter.cc",
"shell/common/gin_converters/osr_converter.h",
"shell/common/gin_converters/serial_port_info_converter.h",
"shell/common/gin_converters/service_worker_converter.cc",
"shell/common/gin_converters/service_worker_converter.cc",
"shell/common/gin_converters/service_worker_converter.h",
"shell/common/gin_converters/std_converter.h",
"shell/common/gin_converters/time_converter.cc",
@@ -666,6 +669,8 @@ filenames = {
"shell/common/gin_helper/pinnable.h",
"shell/common/gin_helper/promise.cc",
"shell/common/gin_helper/promise.h",
"shell/common/gin_helper/reply_channel.cc",
"shell/common/gin_helper/reply_channel.h",
"shell/common/gin_helper/trackable_object.cc",
"shell/common/gin_helper/trackable_object.h",
"shell/common/gin_helper/wrappable.cc",
@@ -715,6 +720,8 @@ filenames = {
"shell/renderer/electron_api_service_impl.h",
"shell/renderer/electron_autofill_agent.cc",
"shell/renderer/electron_autofill_agent.h",
"shell/renderer/electron_ipc_native.cc",
"shell/renderer/electron_ipc_native.h",
"shell/renderer/electron_render_frame_observer.cc",
"shell/renderer/electron_render_frame_observer.h",
"shell/renderer/electron_renderer_client.cc",
@@ -727,6 +734,8 @@ filenames = {
"shell/renderer/preload_utils.h",
"shell/renderer/renderer_client_base.cc",
"shell/renderer/renderer_client_base.h",
"shell/renderer/service_worker_data.cc",
"shell/renderer/service_worker_data.h",
"shell/renderer/web_worker_observer.cc",
"shell/renderer/web_worker_observer.h",
"shell/services/node/node_service.cc",

View File

@@ -1,4 +1,5 @@
import { fetchWithSession } from '@electron/internal/browser/api/net-fetch';
import { addIpcDispatchListeners } from '@electron/internal/browser/ipc-dispatch';
import * as deprecate from '@electron/internal/common/deprecate';
import { net } from 'electron/main';
@@ -21,6 +22,10 @@ Object.defineProperty(systemPickerVideoSource, 'id', {
systemPickerVideoSource.name = '';
Object.freeze(systemPickerVideoSource);
Session.prototype._init = function () {
addIpcDispatchListeners(this, this.serviceWorkers);
};
Session.prototype.fetch = function (input: RequestInfo, init?: RequestInit) {
return fetchWithSession(input, init, this, net.request);
};

View File

@@ -69,6 +69,7 @@ const assertChromeDevTools = function (contents: Electron.WebContents, api: stri
ipcMainInternal.handle(IPC_MESSAGES.INSPECTOR_CONTEXT_MENU, function (event, items: ContextMenuItem[], isEditMenu: boolean) {
return new Promise<number | void>(resolve => {
if (event.type !== 'frame') return;
assertChromeDevTools(event.sender, 'window.InspectorFrontendHost.showContextMenuAtPoint()');
const template = isEditMenu ? getEditMenuItems() : convertToMenuTemplate(items, resolve);
@@ -80,6 +81,7 @@ ipcMainInternal.handle(IPC_MESSAGES.INSPECTOR_CONTEXT_MENU, function (event, ite
});
ipcMainInternal.handle(IPC_MESSAGES.INSPECTOR_SELECT_FILE, async function (event) {
if (event.type !== 'frame') return [];
assertChromeDevTools(event.sender, 'window.UI.createFileSelectorElement()');
const result = await dialog.showOpenDialog({});
@@ -92,6 +94,7 @@ ipcMainInternal.handle(IPC_MESSAGES.INSPECTOR_SELECT_FILE, async function (event
});
ipcMainUtils.handleSync(IPC_MESSAGES.INSPECTOR_CONFIRM, async function (event, message: string = '', title: string = '') {
if (event.type !== 'frame') return;
assertChromeDevTools(event.sender, 'window.confirm()');
const options = {

View File

@@ -267,9 +267,10 @@ const isWebViewTagEnabled = function (contents: Electron.WebContents) {
};
const makeSafeHandler = function<Event extends { sender: Electron.WebContents }> (channel: string, handler: (event: Event, ...args: any[]) => any) {
return (event: Event, ...args: any[]) => {
return (event: Electron.IpcMainInvokeEvent | Electron.IpcMainServiceWorkerInvokeEvent, ...args: any[]) => {
if (event.type !== 'frame') return;
if (isWebViewTagEnabled(event.sender)) {
return handler(event, ...args);
return handler(event as unknown as Event, ...args);
} else {
console.error(`<webview> IPC message ${channel} sent by WebContents with <webview> disabled (${event.sender.id})`);
throw new Error('<webview> disabled');
@@ -281,7 +282,7 @@ const handleMessage = function (channel: string, handler: (event: Electron.IpcMa
ipcMainInternal.handle(channel, makeSafeHandler(channel, handler));
};
const handleMessageSync = function (channel: string, handler: (event: ElectronInternal.IpcMainInternalEvent, ...args: any[]) => any) {
const handleMessageSync = function (channel: string, handler: (event: { sender: Electron.WebContents }, ...args: any[]) => any) {
ipcMainUtils.handleSync(channel, makeSafeHandler(channel, handler));
};
@@ -294,8 +295,10 @@ handleMessageSync(IPC_MESSAGES.GUEST_VIEW_MANAGER_DETACH_GUEST, function (event,
});
// this message is sent by the actual <webview>
ipcMainInternal.on(IPC_MESSAGES.GUEST_VIEW_MANAGER_FOCUS_CHANGE, function (event: ElectronInternal.IpcMainInternalEvent, focus: boolean) {
event.sender.emit('-focus-change', {}, focus);
ipcMainInternal.on(IPC_MESSAGES.GUEST_VIEW_MANAGER_FOCUS_CHANGE, function (event, focus: boolean) {
if (event.type === 'frame') {
event.sender.emit('-focus-change', {}, focus);
}
});
handleMessage(IPC_MESSAGES.GUEST_VIEW_MANAGER_CALL, function (event, guestInstanceId: number, method: string, args: any[]) {

View File

@@ -0,0 +1,91 @@
import { ipcMainInternal } from '@electron/internal/browser/ipc-main-internal';
import { MessagePortMain } from '@electron/internal/browser/message-port-main';
import type { ServiceWorkerMain } from 'electron/main';
const v8Util = process._linkedBinding('electron_common_v8_util');
const addReturnValueToEvent = (event: Electron.IpcMainEvent | Electron.IpcMainServiceWorkerEvent) => {
Object.defineProperty(event, 'returnValue', {
set: (value) => event._replyChannel.sendReply(value),
get: () => {}
});
};
/**
* Listens for IPC dispatch events on `api`.
*
* NOTE: Currently this only supports dispatching IPCs for ServiceWorkerMain.
*/
export function addIpcDispatchListeners (api: NodeJS.EventEmitter, serviceWorkers: Electron.ServiceWorkers) {
const getServiceWorkerFromEvent = (event: Electron.IpcMainServiceWorkerEvent | Electron.IpcMainServiceWorkerInvokeEvent): ServiceWorkerMain | undefined => {
return serviceWorkers._getWorkerFromVersionIDIfExists(event.versionId);
};
const addServiceWorkerPropertyToEvent = (event: Electron.IpcMainServiceWorkerEvent | Electron.IpcMainServiceWorkerInvokeEvent) => {
Object.defineProperty(event, 'serviceWorker', {
get: () => serviceWorkers.getWorkerFromVersionID(event.versionId)
});
};
api.on('-ipc-message' as any, function (event: Electron.IpcMainEvent | Electron.IpcMainServiceWorkerEvent, channel: string, args: any[]) {
const internal = v8Util.getHiddenValue<boolean>(event, 'internal');
if (internal) {
ipcMainInternal.emit(channel, event, ...args);
} else if (event.type === 'service-worker') {
addServiceWorkerPropertyToEvent(event);
getServiceWorkerFromEvent(event)?.ipc.emit(channel, event, ...args);
}
} as any);
api.on('-ipc-invoke' as any, async function (event: Electron.IpcMainInvokeEvent | Electron.IpcMainServiceWorkerInvokeEvent, channel: string, args: any[]) {
const internal = v8Util.getHiddenValue<boolean>(event, 'internal');
const replyWithResult = (result: any) => event._replyChannel.sendReply({ result });
const replyWithError = (error: Error) => {
console.error(`Error occurred in handler for '${channel}':`, error);
event._replyChannel.sendReply({ error: error.toString() });
};
const targets: (Electron.IpcMainServiceWorker | ElectronInternal.IpcMainInternal | undefined)[] = [];
if (internal) {
targets.push(ipcMainInternal);
} else if (event.type === 'service-worker') {
addServiceWorkerPropertyToEvent(event);
const workerIpc = getServiceWorkerFromEvent(event)?.ipc;
targets.push(workerIpc);
}
const target = targets.find(target => (target as any)?._invokeHandlers.has(channel));
if (target) {
const handler = (target as any)._invokeHandlers.get(channel);
try {
replyWithResult(await Promise.resolve(handler(event, ...args)));
} catch (err) {
replyWithError(err as Error);
}
} else {
replyWithError(new Error(`No handler registered for '${channel}'`));
}
} as any);
api.on('-ipc-message-sync' as any, function (event: Electron.IpcMainEvent | Electron.IpcMainServiceWorkerEvent, channel: string, args: any[]) {
const internal = v8Util.getHiddenValue<boolean>(event, 'internal');
addReturnValueToEvent(event);
if (internal) {
ipcMainInternal.emit(channel, event, ...args);
} else if (event.type === 'service-worker') {
addServiceWorkerPropertyToEvent(event);
getServiceWorkerFromEvent(event)?.ipc.emit(channel, event, ...args);
}
} as any);
api.on('-ipc-ports' as any, function (event: Electron.IpcMainEvent | Electron.IpcMainServiceWorkerEvent, channel: string, message: any, ports: any[]) {
event.ports = ports.map(p => new MessagePortMain(p));
if (event.type === 'service-worker') {
addServiceWorkerPropertyToEvent(event);
getServiceWorkerFromEvent(event)?.ipc.emit(channel, event, message);
}
} as any);
}

View File

@@ -19,7 +19,7 @@ export function invokeInWebContents<T> (sender: Electron.WebContents, command: s
const requestId = ++nextId;
const channel = `${command}_RESPONSE_${requestId}`;
ipcMainInternal.on(channel, function handler (event, error: Error, result: any) {
if (event.sender !== sender) {
if (event.type === 'frame' && event.sender !== sender) {
console.error(`Reply to ${command} sent by unexpected WebContents (${event.sender.id})`);
return;
}

View File

@@ -9,6 +9,8 @@ import * as path from 'path';
// Implements window.close()
ipcMainInternal.on(IPC_MESSAGES.BROWSER_WINDOW_CLOSE, function (event) {
if (event.type !== 'frame') return;
const window = event.sender.getOwnerBrowserWindow();
if (window) {
window.close();
@@ -17,10 +19,12 @@ ipcMainInternal.on(IPC_MESSAGES.BROWSER_WINDOW_CLOSE, function (event) {
});
ipcMainInternal.handle(IPC_MESSAGES.BROWSER_GET_LAST_WEB_PREFERENCES, function (event) {
if (event.type !== 'frame') return;
return event.sender.getLastWebPreferences();
});
ipcMainInternal.handle(IPC_MESSAGES.BROWSER_GET_PROCESS_MEMORY_INFO, function (event) {
if (event.type !== 'frame') return;
return event.sender._getProcessMemoryInfo();
});
@@ -101,5 +105,6 @@ ipcMainUtils.handleSync(IPC_MESSAGES.BROWSER_NONSANDBOX_LOAD, function (event) {
});
ipcMainInternal.on(IPC_MESSAGES.BROWSER_PRELOAD_ERROR, function (event, preloadPath: string, error: Error) {
event.sender.emit('preload-error', event, preloadPath, error);
if (event.type !== 'frame') return;
event.sender?.emit('preload-error', event, preloadPath, error);
});

View File

@@ -1,27 +1,16 @@
import { ipcRendererInternal } from '@electron/internal/renderer/ipc-renderer-internal';
import type * as securityWarningsModule from '@electron/internal/renderer/security-warnings';
import type * as webFrameInitModule from '@electron/internal/renderer/web-frame-init';
import type * as webViewInitModule from '@electron/internal/renderer/web-view/web-view-init';
import type * as windowSetupModule from '@electron/internal/renderer/window-setup';
import { ipcRenderer } from 'electron/renderer';
const { mainFrame } = process._linkedBinding('electron_renderer_web_frame');
const v8Util = process._linkedBinding('electron_common_v8_util');
const nodeIntegration = mainFrame.getWebPreference('nodeIntegration');
const webviewTag = mainFrame.getWebPreference('webviewTag');
const isHiddenPage = mainFrame.getWebPreference('hiddenPage');
const isWebView = mainFrame.getWebPreference('isWebView');
// ElectronApiServiceImpl will look for the "ipcNative" hidden object when
// invoking the 'onMessage' callback.
v8Util.setHiddenValue(global, 'ipcNative', {
onMessage (internal: boolean, channel: string, ports: MessagePort[], args: any[]) {
const sender = internal ? ipcRendererInternal : ipcRenderer;
sender.emit(channel, { sender, ports }, ...args);
}
});
require('@electron/internal/renderer/ipc-native-setup');
switch (window.location.protocol) {
case 'devtools:': {

View File

@@ -0,0 +1,14 @@
import { ipcRendererInternal } from '@electron/internal/renderer/ipc-renderer-internal';
import { ipcRenderer } from 'electron/renderer';
const v8Util = process._linkedBinding('electron_common_v8_util');
// ElectronApiServiceImpl will look for the "ipcNative" hidden object when
// invoking the 'onMessage' callback.
v8Util.setHiddenValue(globalThis, 'ipcNative', {
onMessage (internal: boolean, channel: string, ports: MessagePort[], args: any[]) {
const sender = internal ? ipcRendererInternal : ipcRenderer;
sender.emit(channel, { sender, ports }, ...args);
}
});

View File

@@ -1761,6 +1761,12 @@ gin::Handle<Session> Session::CreateFrom(
// to use partition strings, instead of using the Session object directly.
handle->Pin(isolate);
v8::TryCatch try_catch(isolate);
gin_helper::CallMethod(isolate, handle.get(), "_init");
if (try_catch.HasCaught()) {
node::errors::TriggerUncaughtException(isolate, try_catch);
}
App::Get()->EmitWithoutEvent("session-created", handle);
return handle;

View File

@@ -18,6 +18,7 @@
#include "gin/wrappable.h"
#include "services/network/public/mojom/host_resolver.mojom-forward.h"
#include "services/network/public/mojom/ssl_config.mojom-forward.h"
#include "shell/browser/api/ipc_dispatcher.h"
#include "shell/browser/event_emitter_mixin.h"
#include "shell/browser/net/resolve_proxy_helper.h"
#include "shell/common/gin_helper/cleaned_up_at_exit.h"
@@ -66,6 +67,7 @@ class Session final : public gin::Wrappable<Session>,
public gin_helper::Constructible<Session>,
public gin_helper::EventEmitterMixin<Session>,
public gin_helper::CleanedUpAtExit,
public IpcDispatcher<Session>,
#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
private SpellcheckHunspellDictionary::Observer,
#endif

View File

@@ -132,6 +132,7 @@
#include "shell/common/gin_helper/locker.h"
#include "shell/common/gin_helper/object_template_builder.h"
#include "shell/common/gin_helper/promise.h"
#include "shell/common/gin_helper/reply_channel.h"
#include "shell/common/language_util.h"
#include "shell/common/node_includes.h"
#include "shell/common/node_util.h"
@@ -1937,66 +1938,6 @@ void WebContents::OnFirstNonEmptyLayout(
}
}
namespace {
// This object wraps the InvokeCallback so that if it gets GC'd by V8, we can
// still call the callback and send an error. Not doing so causes a Mojo DCHECK,
// since Mojo requires callbacks to be called before they are destroyed.
class ReplyChannel final : public gin::Wrappable<ReplyChannel> {
public:
using InvokeCallback = electron::mojom::ElectronApiIPC::InvokeCallback;
static gin::Handle<ReplyChannel> Create(v8::Isolate* isolate,
InvokeCallback callback) {
return gin::CreateHandle(isolate, new ReplyChannel(std::move(callback)));
}
// gin::Wrappable
static gin::WrapperInfo kWrapperInfo;
gin::ObjectTemplateBuilder GetObjectTemplateBuilder(
v8::Isolate* isolate) override {
return gin::Wrappable<ReplyChannel>::GetObjectTemplateBuilder(isolate)
.SetMethod("sendReply", &ReplyChannel::SendReply);
}
const char* GetTypeName() override { return "ReplyChannel"; }
void SendError(const std::string& msg) {
v8::Isolate* isolate = electron::JavascriptEnvironment::GetIsolate();
// If there's no current context, it means we're shutting down, so we
// don't need to send an event.
if (!isolate->GetCurrentContext().IsEmpty()) {
v8::HandleScope scope(isolate);
auto message = gin::DataObjectBuilder(isolate).Set("error", msg).Build();
SendReply(isolate, message);
}
}
private:
explicit ReplyChannel(InvokeCallback callback)
: callback_(std::move(callback)) {}
~ReplyChannel() override {
if (callback_)
SendError("reply was never sent");
}
bool SendReply(v8::Isolate* isolate, v8::Local<v8::Value> arg) {
if (!callback_)
return false;
blink::CloneableMessage message;
if (!gin::ConvertFromV8(isolate, arg, &message)) {
return false;
}
std::move(callback_).Run(std::move(message));
return true;
}
InvokeCallback callback_;
};
gin::WrapperInfo ReplyChannel::kWrapperInfo = {gin::kEmbedderNativeGin};
} // namespace
gin::Handle<gin_helper::internal::Event> WebContents::MakeEventWithSender(
v8::Isolate* isolate,
content::RenderFrameHost* frame,
@@ -2005,7 +1946,7 @@ gin::Handle<gin_helper::internal::Event> WebContents::MakeEventWithSender(
if (!GetWrapper(isolate).ToLocal(&wrapper)) {
if (callback) {
// We must always invoke the callback if present.
ReplyChannel::Create(isolate, std::move(callback))
gin_helper::internal::ReplyChannel::Create(isolate, std::move(callback))
->SendError("WebContents was destroyed");
}
return {};
@@ -2013,9 +1954,10 @@ gin::Handle<gin_helper::internal::Event> WebContents::MakeEventWithSender(
gin::Handle<gin_helper::internal::Event> event =
gin_helper::internal::Event::New(isolate);
gin_helper::Dictionary dict(isolate, event.ToV8().As<v8::Object>());
dict.Set("type", "frame");
if (callback)
dict.Set("_replyChannel",
ReplyChannel::Create(isolate, std::move(callback)));
dict.Set("_replyChannel", gin_helper::internal::ReplyChannel::Create(
isolate, std::move(callback)));
if (frame) {
dict.SetGetter("senderFrame", frame);
dict.Set("frameId", frame->GetRoutingID());

View File

@@ -0,0 +1,89 @@
// Copyright (c) 2025 Salesforce, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_BROWSER_API_IPC_DISPATCHER_H_
#define ELECTRON_SHELL_BROWSER_API_IPC_DISPATCHER_H_
#include <string>
#include "base/trace_event/trace_event.h"
#include "base/values.h"
#include "gin/handle.h"
#include "shell/browser/api/message_port.h"
#include "shell/browser/javascript_environment.h"
#include "shell/common/api/api.mojom.h"
#include "shell/common/gin_converters/blink_converter.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/event.h"
#include "shell/common/gin_helper/reply_channel.h"
#include "shell/common/v8_util.h"
namespace electron {
// Handles dispatching IPCs to JS.
// See ipc-dispatch.ts for JS listeners.
template <typename T>
class IpcDispatcher {
public:
void Message(gin::Handle<gin_helper::internal::Event>& event,
const std::string& channel,
blink::CloneableMessage args) {
TRACE_EVENT1("electron", "IpcDispatcher::Message", "channel", channel);
emitter()->EmitWithoutEvent("-ipc-message", event, channel, args);
}
void Invoke(gin::Handle<gin_helper::internal::Event>& event,
const std::string& channel,
blink::CloneableMessage arguments,
electron::mojom::ElectronApiIPC::InvokeCallback callback) {
TRACE_EVENT1("electron", "IpcHelper::Invoke", "channel", channel);
v8::Isolate* isolate = electron::JavascriptEnvironment::GetIsolate();
gin_helper::Dictionary dict(isolate, event.ToV8().As<v8::Object>());
dict.Set("_replyChannel", gin_helper::internal::ReplyChannel::Create(
isolate, std::move(callback)));
emitter()->EmitWithoutEvent("-ipc-invoke", event, channel,
std::move(arguments));
}
void ReceivePostMessage(gin::Handle<gin_helper::internal::Event>& event,
const std::string& channel,
blink::TransferableMessage message) {
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
v8::HandleScope handle_scope(isolate);
auto wrapped_ports =
MessagePort::EntanglePorts(isolate, std::move(message.ports));
v8::Local<v8::Value> message_value =
electron::DeserializeV8Value(isolate, message);
emitter()->EmitWithoutEvent("-ipc-ports", event, channel, message_value,
std::move(wrapped_ports));
}
void MessageSync(
gin::Handle<gin_helper::internal::Event>& event,
const std::string& channel,
blink::CloneableMessage arguments,
electron::mojom::ElectronApiIPC::MessageSyncCallback callback) {
TRACE_EVENT1("electron", "IpcHelper::MessageSync", "channel", channel);
v8::Isolate* isolate = electron::JavascriptEnvironment::GetIsolate();
gin_helper::Dictionary dict(isolate, event.ToV8().As<v8::Object>());
dict.Set("_replyChannel", gin_helper::internal::ReplyChannel::Create(
isolate, std::move(callback)));
emitter()->EmitWithoutEvent("-ipc-message-sync", event, channel,
std::move(arguments));
}
private:
inline T* emitter() {
// T must inherit from gin_helper::EventEmitterMixin<T>
return static_cast<T*>(this);
}
};
} // namespace electron
#endif // ELECTRON_SHELL_BROWSER_API_IPC_DISPATCHER_H_

View File

@@ -0,0 +1,199 @@
// Copyright (c) 2025 Salesforce, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/browser/electron_api_sw_ipc_handler_impl.h"
#include <utility>
#include "base/containers/unique_ptr_adapters.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "shell/browser/api/electron_api_session.h"
#include "shell/browser/electron_browser_context.h"
#include "shell/browser/javascript_environment.h"
#include "shell/common/gin_helper/dictionary.h"
namespace electron {
namespace {
const void* const kUserDataKey = &kUserDataKey;
class ServiceWorkerIPCList : public base::SupportsUserData::Data {
public:
std::vector<std::unique_ptr<ElectronApiSWIPCHandlerImpl>> list;
static ServiceWorkerIPCList* Get(
content::RenderProcessHost* render_process_host,
bool create_if_not_exists) {
auto* service_worker_ipc_list = static_cast<ServiceWorkerIPCList*>(
render_process_host->GetUserData(kUserDataKey));
if (!service_worker_ipc_list && !create_if_not_exists) {
return nullptr;
}
if (!service_worker_ipc_list) {
auto new_ipc_list = std::make_unique<ServiceWorkerIPCList>();
service_worker_ipc_list = new_ipc_list.get();
render_process_host->SetUserData(kUserDataKey, std::move(new_ipc_list));
}
return service_worker_ipc_list;
}
};
} // namespace
ElectronApiSWIPCHandlerImpl::ElectronApiSWIPCHandlerImpl(
content::RenderProcessHost* render_process_host,
int64_t version_id,
mojo::PendingAssociatedReceiver<mojom::ElectronApiIPC> receiver)
: render_process_host_(render_process_host), version_id_(version_id) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
receiver_.Bind(std::move(receiver));
receiver_.set_disconnect_handler(
base::BindOnce(&ElectronApiSWIPCHandlerImpl::RemoteDisconnected,
base::Unretained(this)));
render_process_host_->AddObserver(this);
}
ElectronApiSWIPCHandlerImpl::~ElectronApiSWIPCHandlerImpl() {
render_process_host_->RemoveObserver(this);
}
void ElectronApiSWIPCHandlerImpl::RemoteDisconnected() {
receiver_.reset();
Destroy();
}
void ElectronApiSWIPCHandlerImpl::Message(bool internal,
const std::string& channel,
blink::CloneableMessage arguments) {
auto* session = GetSession();
if (session) {
v8::Isolate* isolate = electron::JavascriptEnvironment::GetIsolate();
v8::HandleScope handle_scope(isolate);
gin::Handle<gin_helper::internal::Event> event =
MakeIPCEvent(isolate, internal);
session->Message(event, channel, std::move(arguments));
}
}
void ElectronApiSWIPCHandlerImpl::Invoke(bool internal,
const std::string& channel,
blink::CloneableMessage arguments,
InvokeCallback callback) {
auto* session = GetSession();
if (session) {
v8::Isolate* isolate = electron::JavascriptEnvironment::GetIsolate();
v8::HandleScope handle_scope(isolate);
gin::Handle<gin_helper::internal::Event> event =
MakeIPCEvent(isolate, internal);
session->Invoke(event, channel, std::move(arguments), std::move(callback));
}
}
void ElectronApiSWIPCHandlerImpl::ReceivePostMessage(
const std::string& channel,
blink::TransferableMessage message) {
auto* session = GetSession();
if (session) {
v8::Isolate* isolate = electron::JavascriptEnvironment::GetIsolate();
v8::HandleScope handle_scope(isolate);
gin::Handle<gin_helper::internal::Event> event =
MakeIPCEvent(isolate, false);
session->ReceivePostMessage(event, channel, std::move(message));
}
}
void ElectronApiSWIPCHandlerImpl::MessageSync(bool internal,
const std::string& channel,
blink::CloneableMessage arguments,
MessageSyncCallback callback) {
auto* session = GetSession();
if (session) {
v8::Isolate* isolate = electron::JavascriptEnvironment::GetIsolate();
v8::HandleScope handle_scope(isolate);
gin::Handle<gin_helper::internal::Event> event =
MakeIPCEvent(isolate, internal);
session->MessageSync(event, channel, std::move(arguments),
std::move(callback));
}
}
void ElectronApiSWIPCHandlerImpl::MessageHost(
const std::string& channel,
blink::CloneableMessage arguments) {
NOTIMPLEMENTED(); // Service workers have no <webview>
}
ElectronBrowserContext* ElectronApiSWIPCHandlerImpl::GetBrowserContext() {
auto* browser_context = static_cast<ElectronBrowserContext*>(
render_process_host_->GetBrowserContext());
return browser_context;
}
api::Session* ElectronApiSWIPCHandlerImpl::GetSession() {
return api::Session::FromBrowserContext(GetBrowserContext());
}
gin::Handle<gin_helper::internal::Event>
ElectronApiSWIPCHandlerImpl::MakeIPCEvent(v8::Isolate* isolate, bool internal) {
gin::Handle<gin_helper::internal::Event> event =
gin_helper::internal::Event::New(isolate);
v8::Local<v8::Object> event_object = event.ToV8().As<v8::Object>();
gin_helper::Dictionary dict(isolate, event_object);
dict.Set("type", "service-worker");
dict.Set("versionId", version_id_);
dict.Set("processId", render_process_host_->GetID());
// Set session to provide context for getting preloads
dict.Set("session", GetSession());
if (internal)
dict.SetHidden("internal", internal);
return event;
}
void ElectronApiSWIPCHandlerImpl::Destroy() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
auto* service_worker_ipc_list = ServiceWorkerIPCList::Get(
render_process_host_, /*create_if_not_exists=*/false);
CHECK(service_worker_ipc_list);
// std::erase_if will lead to a call to the destructor for this object.
std::erase_if(service_worker_ipc_list->list, base::MatchesUniquePtr(this));
}
void ElectronApiSWIPCHandlerImpl::RenderProcessExited(
content::RenderProcessHost* host,
const content::ChildProcessTerminationInfo& info) {
CHECK_EQ(host, render_process_host_);
// TODO(crbug.com/1407197): Investigate clearing the user data from
// RenderProcessHostImpl::Cleanup.
Destroy();
// This instance has now been deleted.
}
// static
void ElectronApiSWIPCHandlerImpl::BindReceiver(
int render_process_id,
int64_t version_id,
mojo::PendingAssociatedReceiver<mojom::ElectronApiIPC> receiver) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
auto* render_process_host =
content::RenderProcessHost::FromID(render_process_id);
if (!render_process_host) {
return;
}
auto* service_worker_ipc_list = ServiceWorkerIPCList::Get(
render_process_host, /*create_if_not_exists=*/true);
service_worker_ipc_list->list.push_back(
std::make_unique<ElectronApiSWIPCHandlerImpl>(
render_process_host, version_id, std::move(receiver)));
}
} // namespace electron

View File

@@ -0,0 +1,98 @@
// Copyright (c) 2025 Salesforce, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_BROWSER_ELECTRON_API_SW_IPC_HANDLER_IMPL_H_
#define ELECTRON_SHELL_BROWSER_ELECTRON_API_SW_IPC_HANDLER_IMPL_H_
#include <string>
#include "base/memory/weak_ptr.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_process_host_observer.h"
#include "electron/shell/common/api/api.mojom.h"
#include "gin/handle.h"
#include "mojo/public/cpp/bindings/associated_receiver.h"
#include "shell/common/gin_helper/event.h"
namespace content {
class RenderProcessHost;
}
namespace electron {
class ElectronBrowserContext;
namespace api {
class Session;
}
class ElectronApiSWIPCHandlerImpl : public mojom::ElectronApiIPC,
public content::RenderProcessHostObserver {
public:
explicit ElectronApiSWIPCHandlerImpl(
content::RenderProcessHost* render_process_host,
int64_t version_id,
mojo::PendingAssociatedReceiver<mojom::ElectronApiIPC> receiver);
static void BindReceiver(
int render_process_id,
int64_t version_id,
mojo::PendingAssociatedReceiver<mojom::ElectronApiIPC> receiver);
// disable copy
ElectronApiSWIPCHandlerImpl(const ElectronApiSWIPCHandlerImpl&) = delete;
ElectronApiSWIPCHandlerImpl& operator=(const ElectronApiSWIPCHandlerImpl&) =
delete;
~ElectronApiSWIPCHandlerImpl() override;
// mojom::ElectronApiIPC:
void Message(bool internal,
const std::string& channel,
blink::CloneableMessage arguments) override;
void Invoke(bool internal,
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,
MessageSyncCallback callback) override;
void MessageHost(const std::string& channel,
blink::CloneableMessage arguments) override;
base::WeakPtr<ElectronApiSWIPCHandlerImpl> GetWeakPtr() {
return weak_factory_.GetWeakPtr();
}
private:
ElectronBrowserContext* GetBrowserContext();
api::Session* GetSession();
gin::Handle<gin_helper::internal::Event> MakeIPCEvent(v8::Isolate* isolate,
bool internal);
// content::RenderProcessHostObserver
void RenderProcessExited(
content::RenderProcessHost* host,
const content::ChildProcessTerminationInfo& info) override;
void RemoteDisconnected();
// Destroys this instance by removing it from the ServiceWorkerIPCList.
void Destroy();
// This is safe because ElectronApiSWIPCHandlerImpl is tied to the life time
// of RenderProcessHost.
const raw_ptr<content::RenderProcessHost> render_process_host_;
// Service worker version ID.
int64_t version_id_;
mojo::AssociatedReceiver<mojom::ElectronApiIPC> receiver_{this};
base::WeakPtrFactory<ElectronApiSWIPCHandlerImpl> weak_factory_{this};
};
} // namespace electron
#endif // ELECTRON_SHELL_BROWSER_ELECTRON_API_SW_IPC_HANDLER_IMPL_H_

View File

@@ -79,6 +79,7 @@
#include "shell/browser/bluetooth/electron_bluetooth_delegate.h"
#include "shell/browser/child_web_contents_tracker.h"
#include "shell/browser/electron_api_ipc_handler_impl.h"
#include "shell/browser/electron_api_sw_ipc_handler_impl.h"
#include "shell/browser/electron_autofill_driver_factory.h"
#include "shell/browser/electron_browser_context.h"
#include "shell/browser/electron_browser_main_parts.h"
@@ -1417,6 +1418,13 @@ void ElectronBrowserClient::OverrideURLLoaderFactoryParams(
void ElectronBrowserClient::RegisterAssociatedInterfaceBindersForServiceWorker(
const content::ServiceWorkerVersionBaseInfo& service_worker_version_info,
blink::AssociatedInterfaceRegistry& associated_registry) {
CHECK(service_worker_version_info.process_id !=
content::ChildProcessHost::kInvalidUniqueID);
associated_registry.AddInterface<mojom::ElectronApiIPC>(
base::BindRepeating(&ElectronApiSWIPCHandlerImpl::BindReceiver,
service_worker_version_info.process_id,
service_worker_version_info.version_id));
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
associated_registry.AddInterface<extensions::mojom::RendererHost>(
base::BindRepeating(&extensions::RendererStartupHelper::BindForRenderer,

View File

@@ -0,0 +1,66 @@
// Copyright (c) 2023 Salesforce, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/common/gin_helper/reply_channel.h"
#include "base/debug/stack_trace.h"
#include "gin/data_object_builder.h"
#include "gin/handle.h"
#include "gin/object_template_builder.h"
#include "shell/browser/javascript_environment.h"
#include "shell/common/gin_converters/blink_converter.h"
namespace gin_helper::internal {
// static
using InvokeCallback = electron::mojom::ElectronApiIPC::InvokeCallback;
gin::Handle<ReplyChannel> ReplyChannel::Create(v8::Isolate* isolate,
InvokeCallback callback) {
return gin::CreateHandle(isolate, new ReplyChannel(std::move(callback)));
}
gin::ObjectTemplateBuilder ReplyChannel::GetObjectTemplateBuilder(
v8::Isolate* isolate) {
return gin::Wrappable<ReplyChannel>::GetObjectTemplateBuilder(isolate)
.SetMethod("sendReply", &ReplyChannel::SendReply);
}
const char* ReplyChannel::GetTypeName() {
return "ReplyChannel";
}
ReplyChannel::ReplyChannel(InvokeCallback callback)
: callback_(std::move(callback)) {}
ReplyChannel::~ReplyChannel() {
if (callback_)
SendError("reply was never sent");
}
void ReplyChannel::SendError(const std::string& msg) {
v8::Isolate* isolate = electron::JavascriptEnvironment::GetIsolate();
// If there's no current context, it means we're shutting down, so we
// don't need to send an event.
if (!isolate->GetCurrentContext().IsEmpty()) {
v8::HandleScope scope(isolate);
auto message = gin::DataObjectBuilder(isolate).Set("error", msg).Build();
SendReply(isolate, message);
}
}
bool ReplyChannel::SendReply(v8::Isolate* isolate, v8::Local<v8::Value> arg) {
if (!callback_)
return false;
blink::CloneableMessage message;
if (!gin::ConvertFromV8(isolate, arg, &message)) {
return false;
}
std::move(callback_).Run(std::move(message));
return true;
}
gin::WrapperInfo ReplyChannel::kWrapperInfo = {gin::kEmbedderNativeGin};
} // namespace gin_helper::internal

View File

@@ -0,0 +1,54 @@
// Copyright (c) 2023 Salesforce, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_COMMON_GIN_HELPER_REPLY_CHANNEL_H_
#define ELECTRON_SHELL_COMMON_GIN_HELPER_REPLY_CHANNEL_H_
#include "gin/wrappable.h"
#include "shell/common/api/api.mojom.h"
namespace gin {
template <typename T>
class Handle;
} // namespace gin
namespace v8 {
class Isolate;
template <typename T>
class Local;
class Object;
class ObjectTemplate;
} // namespace v8
namespace gin_helper::internal {
// This object wraps the InvokeCallback so that if it gets GC'd by V8, we can
// still call the callback and send an error. Not doing so causes a Mojo DCHECK,
// since Mojo requires callbacks to be called before they are destroyed.
class ReplyChannel : public gin::Wrappable<ReplyChannel> {
public:
using InvokeCallback = electron::mojom::ElectronApiIPC::InvokeCallback;
static gin::Handle<ReplyChannel> Create(v8::Isolate* isolate,
InvokeCallback callback);
// gin::Wrappable
static gin::WrapperInfo kWrapperInfo;
gin::ObjectTemplateBuilder GetObjectTemplateBuilder(
v8::Isolate* isolate) override;
const char* GetTypeName() override;
void SendError(const std::string& msg);
private:
explicit ReplyChannel(InvokeCallback callback);
~ReplyChannel() override;
bool SendReply(v8::Isolate* isolate, v8::Local<v8::Value> arg);
InvokeCallback callback_;
};
} // namespace gin_helper::internal
#endif // ELECTRON_SHELL_COMMON_GIN_HELPER_REPLY_CHANNEL_H_

View File

@@ -6,6 +6,7 @@
#include "content/public/renderer/render_frame.h"
#include "content/public/renderer/render_frame_observer.h"
#include "content/public/renderer/worker_thread.h"
#include "gin/dictionary.h"
#include "gin/handle.h"
#include "gin/object_template_builder.h"
@@ -20,9 +21,13 @@
#include "shell/common/node_bindings.h"
#include "shell/common/node_includes.h"
#include "shell/common/v8_util.h"
#include "shell/renderer/preload_realm_context.h"
#include "shell/renderer/service_worker_data.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
#include "third_party/blink/public/web/modules/service_worker/web_service_worker_context_proxy.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "third_party/blink/public/web/web_message_port_converter.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h" // nogncheck
using blink::WebLocalFrame;
using content::RenderFrame;
@@ -40,7 +45,16 @@ RenderFrame* GetCurrentRenderFrame() {
return RenderFrame::FromWebFrame(frame);
}
// Thread identifier for the main renderer thread (as opposed to a service
// worker thread).
inline constexpr int kMainThreadId = 0;
bool IsWorkerThread() {
return content::WorkerThread::GetCurrentId() != kMainThreadId;
}
class IPCRenderer final : public gin::Wrappable<IPCRenderer>,
public content::WorkerThread::Observer,
private content::RenderFrameObserver {
public:
static gin::WrapperInfo kWrapperInfo;
@@ -51,14 +65,31 @@ class IPCRenderer final : public gin::Wrappable<IPCRenderer>,
explicit IPCRenderer(v8::Isolate* isolate)
: content::RenderFrameObserver(GetCurrentRenderFrame()) {
RenderFrame* render_frame = GetCurrentRenderFrame();
DCHECK(render_frame);
v8::Local<v8::Context> context = isolate->GetCurrentContext();
blink::ExecutionContext* execution_context =
blink::ExecutionContext::From(context);
if (execution_context->IsWindow()) {
RenderFrame* render_frame = GetCurrentRenderFrame();
DCHECK(render_frame);
render_frame->GetRemoteAssociatedInterfaces()->GetInterface(
&electron_ipc_remote_);
} else if (execution_context->IsShadowRealmGlobalScope()) {
DCHECK(IsWorkerThread());
content::WorkerThread::AddObserver(this);
electron::ServiceWorkerData* service_worker_data =
electron::preload_realm::GetServiceWorkerData(context);
DCHECK(service_worker_data);
service_worker_data->proxy()->GetRemoteAssociatedInterface(
electron_ipc_remote_.BindNewEndpointAndPassReceiver());
} else {
NOTREACHED();
}
weak_context_ =
v8::Global<v8::Context>(isolate, isolate->GetCurrentContext());
weak_context_.SetWeak();
render_frame->GetRemoteAssociatedInterfaces()->GetInterface(
&electron_ipc_remote_);
}
void OnDestruct() override { electron_ipc_remote_.reset(); }
@@ -66,10 +97,13 @@ class IPCRenderer final : public gin::Wrappable<IPCRenderer>,
void WillReleaseScriptContext(v8::Local<v8::Context> context,
int32_t world_id) override {
if (weak_context_.IsEmpty() ||
weak_context_.Get(context->GetIsolate()) == context)
electron_ipc_remote_.reset();
weak_context_.Get(context->GetIsolate()) == context) {
OnDestruct();
}
}
void WillStopCurrentWorkerThread() override { OnDestruct(); }
// gin::Wrappable:
gin::ObjectTemplateBuilder GetObjectTemplateBuilder(
v8::Isolate* isolate) override {

View File

@@ -21,6 +21,7 @@
#include "shell/common/options_switches.h"
#include "shell/common/thread_restrictions.h"
#include "shell/common/v8_util.h"
#include "shell/renderer/electron_ipc_native.h"
#include "shell/renderer/electron_render_frame_observer.h"
#include "shell/renderer/renderer_client_base.h"
#include "third_party/blink/public/mojom/frame/user_activation_notification_type.mojom-shared.h"
@@ -31,73 +32,6 @@
namespace electron {
namespace {
constexpr std::string_view kIpcKey = "ipcNative";
// Gets the private object under kIpcKey
v8::Local<v8::Object> GetIpcObject(v8::Local<v8::Context> context) {
auto* isolate = context->GetIsolate();
auto binding_key = gin::StringToV8(isolate, kIpcKey);
auto private_binding_key = v8::Private::ForApi(isolate, binding_key);
auto global_object = context->Global();
auto value =
global_object->GetPrivate(context, private_binding_key).ToLocalChecked();
if (value.IsEmpty() || !value->IsObject()) {
LOG(ERROR) << "Attempted to get the 'ipcNative' object but it was missing";
return {};
}
return value->ToObject(context).ToLocalChecked();
}
void InvokeIpcCallback(v8::Local<v8::Context> context,
const std::string& callback_name,
std::vector<v8::Local<v8::Value>> args) {
TRACE_EVENT0("devtools.timeline", "FunctionCall");
auto* isolate = context->GetIsolate();
auto ipcNative = GetIpcObject(context);
if (ipcNative.IsEmpty())
return;
// Only set up the node::CallbackScope if there's a node environment.
// Sandboxed renderers don't have a node environment.
std::unique_ptr<node::CallbackScope> callback_scope;
if (node::Environment::GetCurrent(context)) {
callback_scope = std::make_unique<node::CallbackScope>(
isolate, ipcNative, node::async_context{0, 0});
}
auto callback_key = gin::ConvertToV8(isolate, callback_name)
->ToString(context)
.ToLocalChecked();
auto callback_value = ipcNative->Get(context, callback_key).ToLocalChecked();
DCHECK(callback_value->IsFunction()); // set by init.ts
auto callback = callback_value.As<v8::Function>();
std::ignore = callback->Call(context, ipcNative, args.size(), args.data());
}
void EmitIPCEvent(v8::Local<v8::Context> context,
bool internal,
const std::string& channel,
std::vector<v8::Local<v8::Value>> ports,
v8::Local<v8::Value> args) {
auto* isolate = context->GetIsolate();
v8::HandleScope handle_scope(isolate);
v8::Context::Scope context_scope(context);
v8::MicrotasksScope script_scope(isolate, context->GetMicrotaskQueue(),
v8::MicrotasksScope::kRunMicrotasks);
std::vector<v8::Local<v8::Value>> argv = {
gin::ConvertToV8(isolate, internal), gin::ConvertToV8(isolate, channel),
gin::ConvertToV8(isolate, ports), args};
InvokeIpcCallback(context, "onMessage", argv);
}
} // namespace
ElectronApiServiceImpl::~ElectronApiServiceImpl() = default;
ElectronApiServiceImpl::ElectronApiServiceImpl(
@@ -166,7 +100,7 @@ void ElectronApiServiceImpl::Message(bool internal,
v8::Local<v8::Value> args = gin::ConvertToV8(isolate, arguments);
EmitIPCEvent(context, internal, channel, {}, args);
ipc_native::EmitIPCEvent(context, internal, channel, {}, args);
}
void ElectronApiServiceImpl::ReceivePostMessage(
@@ -193,7 +127,8 @@ void ElectronApiServiceImpl::ReceivePostMessage(
std::vector<v8::Local<v8::Value>> args = {message_value};
EmitIPCEvent(context, false, channel, ports, gin::ConvertToV8(isolate, args));
ipc_native::EmitIPCEvent(context, false, channel, ports,
gin::ConvertToV8(isolate, args));
}
void ElectronApiServiceImpl::TakeHeapSnapshot(

View File

@@ -0,0 +1,84 @@
// Copyright (c) 2019 Slack Technologies, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "electron/shell/renderer/electron_ipc_native.h"
#include "base/trace_event/trace_event.h"
#include "shell/common/gin_converters/blink_converter.h"
#include "shell/common/gin_converters/value_converter.h"
#include "shell/common/node_includes.h"
#include "shell/common/v8_util.h"
#include "third_party/blink/public/web/blink.h"
#include "third_party/blink/public/web/web_message_port_converter.h"
namespace electron::ipc_native {
namespace {
constexpr std::string_view kIpcKey = "ipcNative";
// Gets the private object under kIpcKey
v8::Local<v8::Object> GetIpcObject(const v8::Local<v8::Context>& context) {
auto* isolate = context->GetIsolate();
auto binding_key = gin::StringToV8(isolate, kIpcKey);
auto private_binding_key = v8::Private::ForApi(isolate, binding_key);
auto global_object = context->Global();
auto value =
global_object->GetPrivate(context, private_binding_key).ToLocalChecked();
if (value.IsEmpty() || !value->IsObject()) {
LOG(ERROR) << "Attempted to get the 'ipcNative' object but it was missing";
return {};
}
return value->ToObject(context).ToLocalChecked();
}
void InvokeIpcCallback(const v8::Local<v8::Context>& context,
const std::string& callback_name,
std::vector<v8::Local<v8::Value>> args) {
TRACE_EVENT0("devtools.timeline", "FunctionCall");
auto* isolate = context->GetIsolate();
auto ipcNative = GetIpcObject(context);
if (ipcNative.IsEmpty())
return;
// Only set up the node::CallbackScope if there's a node environment.
// Sandboxed renderers don't have a node environment.
std::unique_ptr<node::CallbackScope> callback_scope;
if (node::Environment::GetCurrent(context)) {
callback_scope = std::make_unique<node::CallbackScope>(
isolate, ipcNative, node::async_context{0, 0});
}
auto callback_key = gin::ConvertToV8(isolate, callback_name)
->ToString(context)
.ToLocalChecked();
auto callback_value = ipcNative->Get(context, callback_key).ToLocalChecked();
DCHECK(callback_value->IsFunction()); // set by init.ts
auto callback = callback_value.As<v8::Function>();
std::ignore = callback->Call(context, ipcNative, args.size(), args.data());
}
} // namespace
void EmitIPCEvent(const v8::Local<v8::Context>& context,
bool internal,
const std::string& channel,
std::vector<v8::Local<v8::Value>> ports,
v8::Local<v8::Value> args) {
auto* isolate = context->GetIsolate();
v8::HandleScope handle_scope(isolate);
v8::Context::Scope context_scope(context);
v8::MicrotasksScope script_scope(isolate, context->GetMicrotaskQueue(),
v8::MicrotasksScope::kRunMicrotasks);
std::vector<v8::Local<v8::Value>> argv = {
gin::ConvertToV8(isolate, internal), gin::ConvertToV8(isolate, channel),
gin::ConvertToV8(isolate, ports), args};
InvokeIpcCallback(context, "onMessage", argv);
}
} // namespace electron::ipc_native

View File

@@ -0,0 +1,22 @@
// Copyright (c) 2019 Slack Technologies, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_RENDERER_ELECTRON_IPC_NATIVE_H_
#define ELECTRON_SHELL_RENDERER_ELECTRON_IPC_NATIVE_H_
#include <vector>
#include "v8/include/v8-forward.h"
namespace electron::ipc_native {
void EmitIPCEvent(const v8::Local<v8::Context>& context,
bool internal,
const std::string& channel,
std::vector<v8::Local<v8::Value>> ports,
v8::Local<v8::Value> args);
} // namespace electron::ipc_native
#endif // ELECTRON_SHELL_RENDERER_ELECTRON_IPC_NATIVE_H_

View File

@@ -23,6 +23,7 @@
#include "shell/renderer/electron_render_frame_observer.h"
#include "shell/renderer/preload_realm_context.h"
#include "shell/renderer/preload_utils.h"
#include "shell/renderer/service_worker_data.h"
#include "third_party/blink/public/common/web_preferences/web_preferences.h"
#include "third_party/blink/public/platform/scheduler/web_agent_group_scheduler.h"
#include "third_party/blink/public/web/blink.h"
@@ -33,6 +34,9 @@ namespace electron {
namespace {
// Data which only lives on the service worker's thread
constinit thread_local ServiceWorkerData* service_worker_data = nullptr;
constexpr std::string_view kEmitProcessEventKey = "emit-process-event";
void InvokeEmitProcessEvent(v8::Local<v8::Context> context,
@@ -184,6 +188,11 @@ void ElectronSandboxedRendererClient::WillEvaluateServiceWorkerOnWorkerThread(
auto* command_line = base::CommandLine::ForCurrentProcess();
if (command_line->HasSwitch(switches::kServiceWorkerPreload)) {
if (!service_worker_data) {
service_worker_data = new ServiceWorkerData(
context_proxy, service_worker_version_id, v8_context);
}
preload_realm::OnCreatePreloadableV8Context(v8_context,
service_worker_data);
}
@@ -195,6 +204,13 @@ void ElectronSandboxedRendererClient::
int64_t service_worker_version_id,
const GURL& service_worker_scope,
const GURL& script_url) {
if (service_worker_data) {
DCHECK_EQ(service_worker_version_id,
service_worker_data->service_worker_version_id());
delete service_worker_data;
service_worker_data = nullptr;
}
RendererClientBase::WillDestroyServiceWorkerContextOnWorkerThread(
context, service_worker_version_id, service_worker_scope, script_url);
}

View File

@@ -0,0 +1,72 @@
// Copyright (c) 2025 Salesforce, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "electron/shell/renderer/service_worker_data.h"
#include "shell/common/gin_converters/blink_converter.h"
#include "shell/common/gin_converters/value_converter.h"
#include "shell/common/heap_snapshot.h"
#include "shell/renderer/electron_ipc_native.h"
#include "shell/renderer/preload_realm_context.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_registry.h"
namespace electron {
ServiceWorkerData::~ServiceWorkerData() = default;
ServiceWorkerData::ServiceWorkerData(blink::WebServiceWorkerContextProxy* proxy,
int64_t service_worker_version_id,
const v8::Local<v8::Context>& v8_context)
: proxy_(proxy),
service_worker_version_id_(service_worker_version_id),
isolate_(v8_context->GetIsolate()),
v8_context_(v8_context->GetIsolate(), v8_context) {
proxy_->GetAssociatedInterfaceRegistry()
.AddInterface<mojom::ElectronRenderer>(
base::BindRepeating(&ServiceWorkerData::OnElectronRendererRequest,
weak_ptr_factory_.GetWeakPtr()));
}
void ServiceWorkerData::OnElectronRendererRequest(
mojo::PendingAssociatedReceiver<mojom::ElectronRenderer> receiver) {
receiver_.reset();
receiver_.Bind(std::move(receiver));
}
void ServiceWorkerData::Message(bool internal,
const std::string& channel,
blink::CloneableMessage arguments) {
v8::Isolate* isolate = isolate_.get();
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Context> context = v8_context_.Get(isolate_);
v8::MaybeLocal<v8::Context> maybe_preload_context =
preload_realm::GetPreloadRealmContext(context);
if (maybe_preload_context.IsEmpty()) {
return;
}
v8::Local<v8::Context> preload_context =
maybe_preload_context.ToLocalChecked();
v8::Context::Scope context_scope(preload_context);
v8::Local<v8::Value> args = gin::ConvertToV8(isolate, arguments);
ipc_native::EmitIPCEvent(preload_context, internal, channel, {}, args);
}
void ServiceWorkerData::ReceivePostMessage(const std::string& channel,
blink::TransferableMessage message) {
NOTIMPLEMENTED();
}
void ServiceWorkerData::TakeHeapSnapshot(mojo::ScopedHandle file,
TakeHeapSnapshotCallback callback) {
NOTIMPLEMENTED();
std::move(callback).Run(false);
}
} // namespace electron

View File

@@ -0,0 +1,68 @@
// Copyright (c) 2025 Salesforce, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_RENDERER_SERVICE_WORKER_DATA_H_
#define ELECTRON_SHELL_RENDERER_SERVICE_WORKER_DATA_H_
#include <string>
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "electron/shell/common/api/api.mojom.h"
#include "mojo/public/cpp/bindings/associated_receiver.h"
#include "mojo/public/cpp/bindings/associated_remote.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "third_party/blink/public/web/modules/service_worker/web_service_worker_context_proxy.h"
#include "v8/include/v8-context.h"
#include "v8/include/v8-forward.h"
namespace electron {
// Per ServiceWorker data in worker thread.
class ServiceWorkerData : public mojom::ElectronRenderer {
public:
ServiceWorkerData(blink::WebServiceWorkerContextProxy* proxy,
int64_t service_worker_version_id,
const v8::Local<v8::Context>& v8_context);
~ServiceWorkerData() override;
// disable copy
ServiceWorkerData(const ServiceWorkerData&) = delete;
ServiceWorkerData& operator=(const ServiceWorkerData&) = delete;
int64_t service_worker_version_id() const {
return service_worker_version_id_;
}
blink::WebServiceWorkerContextProxy* proxy() const { return proxy_; }
// mojom::ElectronRenderer
void Message(bool internal,
const std::string& channel,
blink::CloneableMessage arguments) override;
void ReceivePostMessage(const std::string& channel,
blink::TransferableMessage message) override;
void TakeHeapSnapshot(mojo::ScopedHandle file,
TakeHeapSnapshotCallback callback) override;
private:
void OnElectronRendererRequest(
mojo::PendingAssociatedReceiver<mojom::ElectronRenderer> receiver);
raw_ptr<blink::WebServiceWorkerContextProxy> proxy_;
const int64_t service_worker_version_id_;
// The v8 context the bindings are accessible to.
raw_ptr<v8::Isolate> isolate_;
v8::Global<v8::Context> v8_context_;
mojo::AssociatedReceiver<mojom::ElectronRenderer> receiver_{this};
base::WeakPtrFactory<ServiceWorkerData> weak_ptr_factory_{this};
};
} // namespace electron
#endif // ELECTRON_SHELL_RENDERER_SERVICE_WORKER_DATA_H_

View File

@@ -78,6 +78,9 @@ declare namespace Electron {
_countExternalRequests(): number;
}
interface Session {
_init(): void;
}
interface TouchBar {
_removeFromWindow: (win: BaseWindow) => void;
@@ -197,6 +200,14 @@ declare namespace Electron {
frameTreeNodeId?: number;
}
interface IpcMainServiceWorkerEvent {
_replyChannel: ReplyChannel;
}
interface IpcMainServiceWorkerInvokeEvent {
_replyChannel: ReplyChannel;
}
// Deprecated / undocumented BrowserWindow methods
interface BrowserWindow {
getURL(): string;
@@ -272,11 +283,11 @@ declare namespace ElectronInternal {
invoke<T>(channel: string, ...args: any[]): Promise<T>;
}
interface IpcMainInternalEvent extends Omit<Electron.IpcMainEvent, 'reply'> {
}
type IpcMainInternalEvent = Omit<Electron.IpcMainEvent, 'reply'> | Omit<Electron.IpcMainServiceWorkerEvent, 'reply'>;
type IpcMainInternalInvokeEvent = Electron.IpcMainInvokeEvent | Electron.IpcMainServiceWorkerInvokeEvent;
interface IpcMainInternal extends NodeJS.EventEmitter {
handle(channel: string, listener: (event: Electron.IpcMainInvokeEvent, ...args: any[]) => Promise<any> | any): void;
handle(channel: string, listener: (event: IpcMainInternalInvokeEvent, ...args: any[]) => Promise<any> | any): void;
on(channel: string, listener: (event: IpcMainInternalEvent, ...args: any[]) => void): this;
once(channel: string, listener: (event: IpcMainInternalEvent, ...args: any[]) => void): this;
}