Compare commits

...

9 Commits

Author SHA1 Message Date
Samuel Maddock
3c2d4f626a refactor: split IPCRenderer implementation 2025-01-14 18:24:24 -05:00
Samuel Maddock
388469f6b5 docs: direct readers to IpcMain 2025-01-13 16:41:06 -05:00
Samuel Maddock
c8aa24969e fix: proxy bridge arguments only once 2025-01-13 16:25:14 -05:00
Samuel Maddock
a5dc2bf113 test: new service worker features 2025-01-10 11:41:07 -05:00
Samuel Maddock
0534f9f187 feat: service worker IPC 2025-01-10 11:41:07 -05:00
Samuel Maddock
f59d8d618a feat: ServiceWorkerMain 2025-01-10 11:41:06 -05:00
Samuel Maddock
6a730a8e80 feat: contextBridge.executeInMainWorld 2025-01-10 11:39:45 -05:00
Samuel Maddock
e420260647 feat: preload scripts for service workers 2025-01-10 11:39:18 -05:00
Samuel Maddock
ed33b9ddca feat: redesign preload APIs 2025-01-10 11:39:18 -05:00
92 changed files with 4308 additions and 606 deletions

View File

@@ -224,11 +224,21 @@ webpack_build("electron_utility_bundle") {
out_file = "$target_gen_dir/js2c/utility_init.js"
}
webpack_build("electron_preload_realm_bundle") {
deps = [ ":build_electron_definitions" ]
inputs = auto_filenames.preload_realm_bundle_deps
config_file = "//electron/build/webpack/webpack.config.preload_realm.js"
out_file = "$target_gen_dir/js2c/preload_realm_bundle.js"
}
action("electron_js2c") {
deps = [
":electron_browser_bundle",
":electron_isolated_renderer_bundle",
":electron_node_bundle",
":electron_preload_realm_bundle",
":electron_renderer_bundle",
":electron_sandboxed_renderer_bundle",
":electron_utility_bundle",
@@ -240,6 +250,7 @@ action("electron_js2c") {
"$target_gen_dir/js2c/browser_init.js",
"$target_gen_dir/js2c/isolated_bundle.js",
"$target_gen_dir/js2c/node_init.js",
"$target_gen_dir/js2c/preload_realm_bundle.js",
"$target_gen_dir/js2c/renderer_init.js",
"$target_gen_dir/js2c/sandbox_bundle.js",
"$target_gen_dir/js2c/utility_init.js",

View File

@@ -0,0 +1,6 @@
module.exports = require('./webpack.config.base')({
target: 'preload_realm',
alwaysHasNode: false,
wrapInitWithProfilingTimeout: true,
wrapInitWithTryCatch: true
});

View File

@@ -128,6 +128,7 @@ These individual tutorials expand on topics discussed in the guide above.
* [pushNotifications](api/push-notifications.md)
* [safeStorage](api/safe-storage.md)
* [screen](api/screen.md)
* [ServiceWorkerMain](api/service-worker-main.md)
* [session](api/session.md)
* [ShareMenu](api/share-menu.md)
* [systemPreferences](api/system-preferences.md)

View File

@@ -61,6 +61,20 @@ The `contextBridge` module has the following methods:
* `apiKey` string - The key to inject the API onto `window` with. The API will be accessible on `window[apiKey]`.
* `api` any - Your API, more information on what this API can be and how it works is available below.
### `contextBridge.executeInMainWorld(executionScript)` _Experimental_
<!-- TODO(samuelmaddock): add generics to map the `args` types to the `func` params -->
* `executionScript` Object
* `func` (...args: any[]) => any - A JavaScript function to execute. This function will be serialized which means
that any bound parameters and execution context will be lost.
* `args` any[] (optional) - An array of arguments to pass to the provided function. These
arguments will be copied between worlds in accordance with
[the table of supported types.](#parameter--error--return-type-support)
Returns `any` - A copy of the resulting value from executing the function in the main world.
[Refer to the table](#parameter--error--return-type-support) on how values are copied between worlds.
## Usage
### API

View File

@@ -0,0 +1,75 @@
## Class: IpcMainServiceWorker
> Communicate asynchronously from the main process to service workers.
Process: [Main](../glossary.md#main-process)
> [!NOTE]
> This API is a subtle variation of [`IpcMain`](ipc-main.md)—targeted for
> communicating with service workers. For communicating with web frames,
> consult the `IpcMain` documentation.
<!-- TODO(samuelmaddock): refactor doc gen to allow generics to reduce duplication -->
### 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

@@ -114,6 +114,7 @@ A `string` representing the current process's type, can be:
* `browser` - The main process
* `renderer` - A renderer process
* `service-worker` - In a service worker
* `worker` - In a web worker
* `utility` - In a node process launched as a service

View File

@@ -0,0 +1,75 @@
# ServiceWorkerMain
> An instance of a Service Worker representing a version of a script for a given scope.
Process: [Main](../glossary.md#main-process)
## Class: ServiceWorkerMain
Process: [Main](../glossary.md#main-process)<br />
_This class is not exported from the `'electron'` module. It is only available as a return value of other methods in the Electron API._
### Instance Methods
#### `serviceWorker.isDestroyed()` _Experimental_
Returns `boolean` - Whether the service worker has been destroyed.
#### `serviceWorker.send(channel, ...args)` _Experimental_
- `channel` string
- `...args` any[]
Send an asynchronous message to the service worker 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 included.
Sending Functions, Promises, Symbols, WeakMaps, or WeakSets will throw an exception.
The service worker process can handle the message by listening to `channel` with the
[`ipcRenderer`](ipc-renderer.md) module.
#### `serviceWorker.startTask()` _Experimental_
Returns `Object`:
- `end` Function - Method to call when the task has ended. If never called, the service won't terminate while otherwise idle.
Initiate a task to keep the service worker alive until ended.
```js
const { session } = require('electron')
const { serviceWorkers } = session.defaultSession
async function fetchData () {}
const versionId = 0
const serviceWorker = serviceWorkers.getWorkerFromVersionID(versionId)
serviceWorker?.ipc.handle('request-data', async () => {
// Keep service worker alive while fetching data
const task = serviceWorker.startTask()
try {
return await fetchData()
} finally {
// Mark task as ended to allow service worker to terminate when idle.
task.end()
}
})
```
### Instance Properties
#### `serviceWorker.ipc` _Readonly_ _Experimental_
An [`IpcMainServiceWorker`](ipc-main-service-worker.md) instance scoped to the service worker.
#### `serviceWorker.scope` _Readonly_ _Experimental_
A `string` representing the scope URL of the service worker.
#### `serviceWorker.versionId` _Readonly_ _Experimental_
A `number` representing the ID of the specific version of the service worker script in its scope.
[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

View File

@@ -56,6 +56,17 @@ Returns:
Emitted when a service worker has been registered. Can occur after a call to [`navigator.serviceWorker.register('/sw.js')`](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register) successfully resolves or when a Chrome extension is loaded.
#### Event: 'running-status-changed' _Experimental_
Returns:
* `details` Event\<\>
* `versionId` number - ID of the updated service worker version
* `runningStatus` string - Running status.
Possible values include `starting`, `running`, `stopping`, or `stopped`.
Emitted when a service worker's running status has changed.
### Instance Methods
The following methods are available on instances of `ServiceWorkers`:
@@ -64,10 +75,55 @@ The following methods are available on instances of `ServiceWorkers`:
Returns `Record<number, ServiceWorkerInfo>` - A [ServiceWorkerInfo](structures/service-worker-info.md) object where the keys are the service worker version ID and the values are the information about that service worker.
#### `serviceWorkers.getFromVersionID(versionId)`
#### `serviceWorkers.getInfoFromVersionID(versionId)`
* `versionId` number
* `versionId` number - ID of the service worker version
Returns [`ServiceWorkerInfo`](structures/service-worker-info.md) - Information about this service worker
If the service worker does not exist or is not running this method will throw an exception.
#### `serviceWorkers.getFromVersionID(versionId)` _Deprecated_
* `versionId` number - ID of the service worker version
Returns [`ServiceWorkerInfo`](structures/service-worker-info.md) - Information about this service worker
If the service worker does not exist or is not running this method will throw an exception.
**Deprecated:** Use the new `serviceWorkers.getInfoFromVersionID` API.
#### `serviceWorkers.getWorkerFromVersionID(versionId)` _Experimental_
* `versionId` number - ID of the service worker version
Returns [`ServiceWorkerMain | undefined`](service-worker-main.md) - Instance of the service worker associated with the given version ID.
#### `serviceWorkers.startWorkerForScope(scope)` _Experimental_
* `scope` string - The scope of the service worker to start.
Returns `Promise<ServiceWorkerMain>` - Resolves with the service worker when it's started.
Starts the service worker or does nothing if already running.
```js
const { app, session } = require('electron')
const { serviceWorkers } = session.defaultSession
// Collect service workers scopes
const workerScopes = Object.values(serviceWorkers.getAllRunning()).map((info) => info.scope)
app.on('browser-window-created', async (event, window) => {
for (const scope of workerScopes) {
try {
// Ensure worker is started and send message
const serviceWorker = await serviceWorkers.startWorkerForScope(scope)
serviceWorker.send('window-created', { windowId: window.id })
} catch (error) {
console.error(`Failed to start service worker for ${scope}`)
console.error(error)
}
}
})
```

View File

@@ -1330,18 +1330,44 @@ the initial state will be `interrupted`. The download will start only when the
Returns `Promise<void>` - resolves when the sessions HTTP authentication cache has been cleared.
#### `ses.setPreloads(preloads)`
#### `ses.setPreloads(preloads)` _Deprecated_
* `preloads` string[] - An array of absolute path to preload scripts
Adds scripts that will be executed on ALL web contents that are associated with
this session just before normal `preload` scripts run.
#### `ses.getPreloads()`
**Deprecated:** Use the new `ses.registerPreloadScript` API. This will overwrite any preload scripts
registered for `service-worker` context types.
#### `ses.getPreloads()` _Deprecated_
Returns `string[]` an array of paths to preload scripts that have been
registered.
**Deprecated:** Use the new `ses.getPreloadScripts` API. This will only return preload script paths
for `frame` context types.
#### `ses.registerPreloadScript(script)`
* `script` [PreloadScriptRegistration](structures/preload-script-registration.md) - Preload script
Registers preload script that will be executed in its associated context type in this session. For
`frame` contexts, this will run prior to any preload defined in the web preferences of a
WebContents.
Returns `string` - The ID of the registered preload script.
#### `ses.unregisterPreloadScript(id)`
* `id` string - Preload script ID
Unregisters script.
#### `ses.getPreloadScripts()`
Returns [`PreloadScript[]`](structures/preload-script.md): An array of paths to preload scripts that have been registered.
#### `ses.setCodeCachePath(path)`
* `path` String - Absolute path to store the v8 generated JS code cache from the renderer.

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

@@ -0,0 +1,6 @@
# PreloadScriptRegistration Object
* `type` string - Context type where the preload script will be executed.
Possible values include `frame` or `service-worker`.
* `id` string (optional) - Unique ID of preload script. Defaults to a random UUID.
* `filePath` string - Path of the script file. Must be an absolute path.

View File

@@ -0,0 +1,6 @@
# PreloadScript Object
* `type` string - Context type where the preload script will be executed.
Possible values include `frame` or `service-worker`.
* `id` string - Unique ID of preload script.
* `filePath` string - Path of the script file. Must be an absolute path.

View File

@@ -3,3 +3,4 @@
* `scriptUrl` string - The full URL to the script that this service worker runs
* `scope` string - The base URL that this service worker is active for.
* `renderProcessId` number - The virtual ID of the process that this service worker is running in. This is not an OS level PID. This aligns with the ID set used for `webContents.getProcessId()`.
* `versionId` number - ID of the service worker version

View File

@@ -12,6 +12,42 @@ This document uses the following convention to categorize breaking changes:
* **Deprecated:** An API was marked as deprecated. The API will continue to function, but will emit a deprecation warning, and will be removed in a future release.
* **Removed:** An API or feature was removed, and is no longer supported by Electron.
## Planned Breaking API Changes (35.0)
### Deprecated: `setPreloads`, `getPreloads` on `Session`
`registerPreloadScript`, `unregisterPreloadScript`, and `getPreloadScripts` are introduced as a
replacement for the deprecated methods. These new APIs allow third-party libraries to register
preload scripts without replacing existing scripts. Also, the new `type` option allows for
additional preload targets such as `service-worker`.
```ts
// Deprecated
session.setPreloads([path.join(__dirname, 'preload.js')])
// Replace with:
session.registerPreloadScript({
type: 'frame',
id: 'app-preload',
filePath: path.join(__dirname, 'preload.js')
})
```
### Deprecated: `getFromVersionID` on `session.serviceWorkers`
The `session.serviceWorkers.fromVersionID(versionId)` API has been deprecated
in favor of `session.serviceWorkers.getInfoFromVersionID(versionId)`. This was
changed to make it more clear which object is returned with the introduction
of the `session.serviceWorkers.getWorkerFromVersionID(versionId)` API.
```js
// Deprecated
session.serviceWorkers.fromVersionID(versionId)
// Replace with
session.serviceWorkers.getInfoFromVersionID(versionId)
```
## Planned Breaking API Changes (34.0)
### Deprecated: `level`, `message`, `line`, and `sourceId` arguments in `console-message` event on `WebContents`

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,6 +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-workers.md",
"docs/api/session.md",
"docs/api/share-menu.md",
@@ -94,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",
@@ -114,6 +118,8 @@ auto_filenames = {
"docs/api/structures/permission-request.md",
"docs/api/structures/point.md",
"docs/api/structures/post-body.md",
"docs/api/structures/preload-script-registration.md",
"docs/api/structures/preload-script.md",
"docs/api/structures/printer-info.md",
"docs/api/structures/process-memory-info.md",
"docs/api/structures/process-metric.md",
@@ -169,6 +175,8 @@ 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-bindings.ts",
"lib/renderer/ipc-renderer-internal-utils.ts",
"lib/renderer/ipc-renderer-internal.ts",
"lib/renderer/security-warnings.ts",
@@ -183,6 +191,8 @@ auto_filenames = {
"lib/sandboxed_renderer/api/exports/electron.ts",
"lib/sandboxed_renderer/api/module-list.ts",
"lib/sandboxed_renderer/init.ts",
"lib/sandboxed_renderer/pre-init.ts",
"lib/sandboxed_renderer/preload.ts",
"package.json",
"tsconfig.electron.json",
"tsconfig.json",
@@ -239,6 +249,7 @@ auto_filenames = {
"lib/browser/api/push-notifications.ts",
"lib/browser/api/safe-storage.ts",
"lib/browser/api/screen.ts",
"lib/browser/api/service-worker-main.ts",
"lib/browser/api/session.ts",
"lib/browser/api/share-menu.ts",
"lib/browser/api/system-preferences.ts",
@@ -255,6 +266,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",
@@ -299,6 +311,8 @@ 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-bindings.ts",
"lib/renderer/ipc-renderer-internal-utils.ts",
"lib/renderer/ipc-renderer-internal.ts",
"lib/renderer/security-warnings.ts",
@@ -333,6 +347,7 @@ auto_filenames = {
"lib/renderer/api/module-list.ts",
"lib/renderer/api/web-frame.ts",
"lib/renderer/api/web-utils.ts",
"lib/renderer/ipc-renderer-bindings.ts",
"lib/renderer/ipc-renderer-internal-utils.ts",
"lib/renderer/ipc-renderer-internal.ts",
"lib/worker/init.ts",
@@ -373,4 +388,27 @@ auto_filenames = {
"typings/internal-ambient.d.ts",
"typings/internal-electron.d.ts",
]
preload_realm_bundle_deps = [
"lib/common/api/native-image.ts",
"lib/common/define-properties.ts",
"lib/common/ipc-messages.ts",
"lib/common/webpack-globals-provider.ts",
"lib/preload_realm/api/exports/electron.ts",
"lib/preload_realm/api/module-list.ts",
"lib/preload_realm/init.ts",
"lib/renderer/api/context-bridge.ts",
"lib/renderer/api/ipc-renderer.ts",
"lib/renderer/ipc-native-setup.ts",
"lib/renderer/ipc-renderer-bindings.ts",
"lib/renderer/ipc-renderer-internal-utils.ts",
"lib/renderer/ipc-renderer-internal.ts",
"lib/sandboxed_renderer/pre-init.ts",
"lib/sandboxed_renderer/preload.ts",
"package.json",
"tsconfig.electron.json",
"tsconfig.json",
"typings/internal-ambient.d.ts",
"typings/internal-electron.d.ts",
]
}

View File

@@ -304,6 +304,8 @@ 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.h",
"shell/browser/api/electron_api_session.cc",
"shell/browser/api/electron_api_session.h",
"shell/browser/api/electron_api_system_preferences.cc",
@@ -330,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",
@@ -361,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",
@@ -482,6 +487,7 @@ filenames = {
"shell/browser/osr/osr_web_contents_view.h",
"shell/browser/plugins/plugin_utils.cc",
"shell/browser/plugins/plugin_utils.h",
"shell/browser/preload_script.h",
"shell/browser/protocol_registry.cc",
"shell/browser/protocol_registry.h",
"shell/browser/relauncher.cc",
@@ -621,6 +627,8 @@ 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.h",
"shell/common/gin_converters/std_converter.h",
"shell/common/gin_converters/time_converter.cc",
"shell/common/gin_converters/time_converter.h",
@@ -661,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",
@@ -710,14 +720,22 @@ 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",
"shell/renderer/electron_renderer_client.h",
"shell/renderer/electron_sandboxed_renderer_client.cc",
"shell/renderer/electron_sandboxed_renderer_client.h",
"shell/renderer/preload_realm_context.cc",
"shell/renderer/preload_realm_context.h",
"shell/renderer/preload_utils.cc",
"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

@@ -29,6 +29,7 @@ export const browserModuleList: ElectronInternal.ModuleEntry[] = [
{ name: 'protocol', loader: () => require('./protocol') },
{ name: 'safeStorage', loader: () => require('./safe-storage') },
{ name: 'screen', loader: () => require('./screen') },
{ name: 'ServiceWorkerMain', loader: () => require('./service-worker-main') },
{ name: 'session', loader: () => require('./session') },
{ name: 'ShareMenu', loader: () => require('./share-menu') },
{ name: 'systemPreferences', loader: () => require('./system-preferences') },

View File

@@ -0,0 +1,39 @@
import { IpcMainImpl } from '@electron/internal/browser/ipc-main-impl';
const { ServiceWorkerMain } = process._linkedBinding('electron_browser_service_worker_main');
Object.defineProperty(ServiceWorkerMain.prototype, 'ipc', {
get () {
const ipc = new IpcMainImpl();
Object.defineProperty(this, 'ipc', { value: ipc });
return ipc;
}
});
ServiceWorkerMain.prototype.send = function (channel, ...args) {
if (typeof channel !== 'string') {
throw new TypeError('Missing required channel argument');
}
try {
return this._send(false /* internal */, channel, args);
} catch (e) {
console.error('Error sending from ServiceWorkerMain: ', e);
}
};
ServiceWorkerMain.prototype.startTask = function () {
// TODO(samuelmaddock): maybe make timeout configurable in the future
const hasTimeout = false;
const { id, ok } = this._startExternalRequest(hasTimeout);
if (!ok) {
throw new Error('Unable to start service worker task.');
}
return {
end: () => this._finishExternalRequest(id)
};
};
module.exports = ServiceWorkerMain;

View File

@@ -1,4 +1,6 @@
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';
@@ -20,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);
};
@@ -36,6 +42,31 @@ Session.prototype.setDisplayMediaRequestHandler = function (handler, opts) {
}, opts);
};
const getPreloadsDeprecated = deprecate.warnOnce('session.getPreloads', 'session.getPreloadScripts');
Session.prototype.getPreloads = function () {
getPreloadsDeprecated();
return this.getPreloadScripts()
.filter((script) => script.type === 'frame')
.map((script) => script.filePath);
};
const setPreloadsDeprecated = deprecate.warnOnce('session.setPreloads', 'session.registerPreloadScript');
Session.prototype.setPreloads = function (preloads) {
setPreloadsDeprecated();
this.getPreloadScripts()
.filter((script) => script.type === 'frame')
.forEach((script) => {
this.unregisterPreloadScript(script.id);
});
preloads.map(filePath => ({
type: 'frame',
filePath,
_deprecated: true
}) as Electron.PreloadScriptRegistration).forEach(script => {
this.registerPreloadScript(script);
});
};
export default {
fromPartition,
fromPath,

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

@@ -146,6 +146,9 @@ require('@electron/internal/browser/devtools');
// Load protocol module to ensure it is populated on app ready
require('@electron/internal/browser/api/protocol');
// Load service-worker-main module to ensure it is populated on app ready
require('@electron/internal/browser/api/service-worker-main');
// Load web-contents module to ensure it is populated on app ready
require('@electron/internal/browser/api/web-contents');

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

@@ -5,9 +5,12 @@ import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages';
import { clipboard } from 'electron/common';
import * as fs from 'fs';
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();
@@ -16,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();
});
@@ -43,22 +48,46 @@ ipcMainUtils.handleSync(IPC_MESSAGES.BROWSER_CLIPBOARD_SYNC, function (event, me
return (clipboard as any)[method](...args);
});
const getPreloadScript = async function (preloadPath: string) {
let preloadSrc = null;
let preloadError = null;
try {
preloadSrc = await fs.promises.readFile(preloadPath, 'utf8');
} catch (error) {
preloadError = error;
const getPreloadScriptsFromEvent = (event: ElectronInternal.IpcMainInternalEvent) => {
const session: Electron.Session = event.type === 'service-worker' ? event.session : event.sender.session;
let preloadScripts = session.getPreloadScripts();
if (event.type === 'frame') {
preloadScripts = preloadScripts.filter(script => script.type === 'frame');
const preload = event.sender._getPreloadScript();
if (preload) preloadScripts.push(preload);
} else if (event.type === 'service-worker') {
preloadScripts = preloadScripts.filter(script => script.type === 'service-worker');
} else {
throw new Error(`getPreloadScriptsFromEvent: event.type is invalid (${(event as any).type})`);
}
return { preloadPath, preloadSrc, preloadError };
// TODO(samuelmaddock): Remove filter after Session.setPreloads is fully
// deprecated. The new API will prevent relative paths from being registered.
return preloadScripts.filter(script => path.isAbsolute(script.filePath));
};
const readPreloadScript = async function (script: Electron.PreloadScript): Promise<ElectronInternal.PreloadScript> {
let contents;
let error;
try {
contents = await fs.promises.readFile(script.filePath, 'utf8');
} catch (err) {
if (err instanceof Error) {
error = err;
}
}
return {
...script,
contents,
error
};
};
ipcMainUtils.handleSync(IPC_MESSAGES.BROWSER_SANDBOX_LOAD, async function (event) {
const preloadPaths = event.sender._getPreloadPaths();
const preloadScripts = getPreloadScriptsFromEvent(event);
return {
preloadScripts: await Promise.all(preloadPaths.map(path => getPreloadScript(path))),
preloadScripts: await Promise.all(preloadScripts.map(readPreloadScript)),
process: {
arch: process.arch,
platform: process.platform,
@@ -71,9 +100,11 @@ ipcMainUtils.handleSync(IPC_MESSAGES.BROWSER_SANDBOX_LOAD, async function (event
});
ipcMainUtils.handleSync(IPC_MESSAGES.BROWSER_NONSANDBOX_LOAD, function (event) {
return { preloadPaths: event.sender._getPreloadPaths() };
const preloadScripts = getPreloadScriptsFromEvent(event);
return { preloadPaths: preloadScripts.map(script => script.filePath) };
});
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

@@ -0,0 +1,18 @@
{
"rules": {
"no-restricted-imports": [
"error",
{
"paths": [
"electron",
"electron/main"
],
"patterns": [
"./*",
"../*",
"@electron/internal/browser/*"
]
}
]
}
}

View File

@@ -0,0 +1,6 @@
import { defineProperties } from '@electron/internal/common/define-properties';
import { moduleList } from '@electron/internal/preload_realm/api/module-list';
module.exports = {};
defineProperties(module.exports, moduleList);

View File

@@ -0,0 +1,14 @@
export const moduleList: ElectronInternal.ModuleEntry[] = [
{
name: 'contextBridge',
loader: () => require('@electron/internal/renderer/api/context-bridge')
},
{
name: 'ipcRenderer',
loader: () => require('@electron/internal/renderer/api/ipc-renderer')
},
{
name: 'nativeImage',
loader: () => require('@electron/internal/common/api/native-image')
}
];

56
lib/preload_realm/init.ts Normal file
View File

@@ -0,0 +1,56 @@
import '@electron/internal/sandboxed_renderer/pre-init';
import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages';
import type * as ipcRendererUtilsModule from '@electron/internal/renderer/ipc-renderer-internal-utils';
import { createPreloadProcessObject, executeSandboxedPreloadScripts } from '@electron/internal/sandboxed_renderer/preload';
import * as events from 'events';
declare const binding: {
get: (name: string) => any;
process: NodeJS.Process;
createPreloadScript: (src: string) => Function
};
const ipcRendererUtils = require('@electron/internal/renderer/ipc-renderer-internal-utils') as typeof ipcRendererUtilsModule;
const {
preloadScripts,
process: processProps
} = ipcRendererUtils.invokeSync<{
preloadScripts: ElectronInternal.PreloadScript[];
process: NodeJS.Process;
}>(IPC_MESSAGES.BROWSER_SANDBOX_LOAD);
const electron = require('electron');
const loadedModules = new Map<string, any>([
['electron', electron],
['electron/common', electron],
['events', events],
['node:events', events]
]);
const loadableModules = new Map<string, Function>([
['url', () => require('url')],
['node:url', () => require('url')]
]);
const preloadProcess = createPreloadProcessObject();
Object.assign(preloadProcess, binding.process);
Object.assign(preloadProcess, processProps);
Object.assign(process, processProps);
require('@electron/internal/renderer/ipc-native-setup');
executeSandboxedPreloadScripts({
loadedModules: loadedModules,
loadableModules: loadableModules,
process: preloadProcess,
createPreloadScript: binding.createPreloadScript,
exposeGlobals: {
Buffer: Buffer,
global: global
}
}, preloadScripts);

View File

@@ -5,13 +5,17 @@ const checkContextIsolationEnabled = () => {
};
const contextBridge: Electron.ContextBridge = {
exposeInMainWorld: (key: string, api: any) => {
exposeInMainWorld: (key, api) => {
checkContextIsolationEnabled();
return binding.exposeAPIInWorld(0, key, api);
},
exposeInIsolatedWorld: (worldId: number, key: string, api: any) => {
exposeInIsolatedWorld: (worldId, key, api) => {
checkContextIsolationEnabled();
return binding.exposeAPIInWorld(worldId, key, api);
},
executeInMainWorld: (script) => {
checkContextIsolationEnabled();
return binding.executeInWorld(0, script);
}
};
@@ -27,8 +31,7 @@ export const internalContextBridge = {
},
overrideGlobalPropertyFromIsolatedWorld: (keys: string[], getter: Function, setter?: Function) => {
return binding._overrideGlobalPropertyFromIsolatedWorld(keys, getter, setter || null);
},
isInMainWorld: () => binding._isCalledFromMainWorld() as boolean
}
};
if (binding._isDebug) {

View File

@@ -1,8 +1,10 @@
import { getIPCRenderer } from '@electron/internal/renderer/ipc-renderer-bindings';
import { EventEmitter } from 'events';
const { ipc } = process._linkedBinding('electron_renderer_ipc');
const ipc = getIPCRenderer();
const internal = false;
class IpcRenderer extends EventEmitter implements Electron.IpcRenderer {
send (channel: string, ...args: any[]) {
return ipc.send(internal, channel, args);

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

@@ -0,0 +1,17 @@
let ipc: NodeJS.IpcRendererImpl | undefined;
/**
* Get IPCRenderer implementation for the current process.
*/
export function getIPCRenderer () {
if (ipc) return ipc;
const ipcBinding = process._linkedBinding('electron_renderer_ipc');
switch (process.type) {
case 'renderer':
return (ipc = ipcBinding.createForRenderFrame());
case 'service-worker':
return (ipc = ipcBinding.createForServiceWorker());
default:
throw new Error(`Cannot create IPCRenderer for '${process.type}' process`);
}
};

View File

@@ -1,7 +1,8 @@
import { getIPCRenderer } from '@electron/internal/renderer/ipc-renderer-bindings';
import { EventEmitter } from 'events';
const { ipc } = process._linkedBinding('electron_renderer_ipc');
const ipc = getIPCRenderer();
const internal = true;
class IpcRendererInternal extends EventEmitter implements ElectronInternal.IpcRendererInternal {

View File

@@ -1,45 +1,23 @@
import '@electron/internal/sandboxed_renderer/pre-init';
import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages';
import type * as ipcRendererInternalModule from '@electron/internal/renderer/ipc-renderer-internal';
import type * as ipcRendererUtilsModule from '@electron/internal/renderer/ipc-renderer-internal-utils';
import { createPreloadProcessObject, executeSandboxedPreloadScripts } from '@electron/internal/sandboxed_renderer/preload';
import * as events from 'events';
import { setImmediate, clearImmediate } from 'timers';
declare const binding: {
get: (name: string) => any;
process: NodeJS.Process;
createPreloadScript: (src: string) => Function
};
const { EventEmitter } = events;
process._linkedBinding = binding.get;
const v8Util = process._linkedBinding('electron_common_v8_util');
// Expose Buffer shim as a hidden value. This is used by C++ code to
// deserialize Buffer instances sent from browser process.
v8Util.setHiddenValue(global, 'Buffer', Buffer);
// The process object created by webpack is not an event emitter, fix it so
// the API is more compatible with non-sandboxed renderers.
for (const prop of Object.keys(EventEmitter.prototype) as (keyof typeof process)[]) {
if (Object.hasOwn(process, prop)) {
delete process[prop];
}
}
Object.setPrototypeOf(process, EventEmitter.prototype);
const { ipcRendererInternal } = require('@electron/internal/renderer/ipc-renderer-internal') as typeof ipcRendererInternalModule;
const ipcRendererUtils = require('@electron/internal/renderer/ipc-renderer-internal-utils') as typeof ipcRendererUtilsModule;
const {
preloadScripts,
process: processProps
} = ipcRendererUtils.invokeSync<{
preloadScripts: {
preloadPath: string;
preloadSrc: string | null;
preloadError: null | Error;
}[];
preloadScripts: ElectronInternal.PreloadScript[];
process: NodeJS.Process;
}>(IPC_MESSAGES.BROWSER_SANDBOX_LOAD);
@@ -60,89 +38,32 @@ const loadableModules = new Map<string, Function>([
['node:url', () => require('url')]
]);
// Pass different process object to the preload script.
const preloadProcess: NodeJS.Process = new EventEmitter() as any;
const preloadProcess = createPreloadProcessObject();
// InvokeEmitProcessEvent in ElectronSandboxedRendererClient will look for this
const v8Util = process._linkedBinding('electron_common_v8_util');
v8Util.setHiddenValue(global, 'emit-process-event', (event: string) => {
(process as events.EventEmitter).emit(event);
(preloadProcess as events.EventEmitter).emit(event);
});
Object.assign(preloadProcess, binding.process);
Object.assign(preloadProcess, processProps);
Object.assign(process, binding.process);
Object.assign(process, processProps);
process.getProcessMemoryInfo = preloadProcess.getProcessMemoryInfo = () => {
return ipcRendererInternal.invoke<Electron.ProcessMemoryInfo>(IPC_MESSAGES.BROWSER_GET_PROCESS_MEMORY_INFO);
};
Object.defineProperty(preloadProcess, 'noDeprecation', {
get () {
return process.noDeprecation;
},
set (value) {
process.noDeprecation = value;
}
});
// This is the `require` function that will be visible to the preload script
function preloadRequire (module: string) {
if (loadedModules.has(module)) {
return loadedModules.get(module);
}
if (loadableModules.has(module)) {
const loadedModule = loadableModules.get(module)!();
loadedModules.set(module, loadedModule);
return loadedModule;
}
throw new Error(`module not found: ${module}`);
}
// Process command line arguments.
const { hasSwitch } = process._linkedBinding('electron_common_command_line');
// Similar to nodes --expose-internals flag, this exposes _linkedBinding so
// that tests can call it to get access to some test only bindings
if (hasSwitch('unsafely-expose-electron-internals-for-testing')) {
preloadProcess._linkedBinding = process._linkedBinding;
}
Object.assign(preloadProcess, binding.process);
Object.assign(preloadProcess, processProps);
// Common renderer initialization
require('@electron/internal/renderer/common-init');
// Wrap the script into a function executed in global scope. It won't have
// access to the current scope, so we'll expose a few objects as arguments:
//
// - `require`: The `preloadRequire` function
// - `process`: The `preloadProcess` object
// - `Buffer`: Shim of `Buffer` implementation
// - `global`: The window object, which is aliased to `global` by webpack.
function runPreloadScript (preloadSrc: string) {
const preloadWrapperSrc = `(function(require, process, Buffer, global, setImmediate, clearImmediate, exports, module) {
${preloadSrc}
})`;
// eval in window scope
const preloadFn = binding.createPreloadScript(preloadWrapperSrc);
const exports = {};
preloadFn(preloadRequire, preloadProcess, Buffer, global, setImmediate, clearImmediate, exports, { exports });
}
for (const { preloadPath, preloadSrc, preloadError } of preloadScripts) {
try {
if (preloadSrc) {
runPreloadScript(preloadSrc);
} else if (preloadError) {
throw preloadError;
}
} catch (error) {
console.error(`Unable to load preload script: ${preloadPath}`);
console.error(error);
ipcRendererInternal.send(IPC_MESSAGES.BROWSER_PRELOAD_ERROR, preloadPath, error);
executeSandboxedPreloadScripts({
loadedModules: loadedModules,
loadableModules: loadableModules,
process: preloadProcess,
createPreloadScript: binding.createPreloadScript,
exposeGlobals: {
Buffer: Buffer,
global: global,
setImmediate: setImmediate,
clearImmediate: clearImmediate
}
}
}, preloadScripts);

View File

@@ -0,0 +1,30 @@
// Pre-initialization code for sandboxed renderers.
import * as events from 'events';
declare const binding: {
get: (name: string) => any;
process: NodeJS.Process;
};
// Expose internal binding getter.
process._linkedBinding = binding.get;
const { EventEmitter } = events;
const v8Util = process._linkedBinding('electron_common_v8_util');
// Include properties from script 'binding' parameter.
Object.assign(process, binding.process);
// Expose Buffer shim as a hidden value. This is used by C++ code to
// deserialize Buffer instances sent from browser process.
v8Util.setHiddenValue(global, 'Buffer', Buffer);
// The process object created by webpack is not an event emitter, fix it so
// the API is more compatible with non-sandboxed renderers.
for (const prop of Object.keys(EventEmitter.prototype) as (keyof typeof process)[]) {
if (Object.hasOwn(process, prop)) {
delete process[prop];
}
}
Object.setPrototypeOf(process, EventEmitter.prototype);

View File

@@ -0,0 +1,107 @@
import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages';
import type * as ipcRendererInternalModule from '@electron/internal/renderer/ipc-renderer-internal';
import { EventEmitter } from 'events';
// Delay loading for `process._linkedBinding` to be set.
const getIpcRendererLazy = () => require('@electron/internal/renderer/ipc-renderer-internal') as typeof ipcRendererInternalModule;
interface PreloadContext {
loadedModules: Map<string, any>;
loadableModules: Map<string, any>;
/** Process object to pass into preloads. */
process: NodeJS.Process;
createPreloadScript: (src: string) => Function
/** Globals to be exposed to preload context. */
exposeGlobals: any;
}
export function createPreloadProcessObject (): NodeJS.Process {
const preloadProcess: NodeJS.Process = new EventEmitter() as any;
preloadProcess.getProcessMemoryInfo = () => {
const { ipcRendererInternal } = getIpcRendererLazy();
return ipcRendererInternal.invoke<Electron.ProcessMemoryInfo>(IPC_MESSAGES.BROWSER_GET_PROCESS_MEMORY_INFO);
};
Object.defineProperty(preloadProcess, 'noDeprecation', {
get () {
return process.noDeprecation;
},
set (value) {
process.noDeprecation = value;
}
});
const { hasSwitch } = process._linkedBinding('electron_common_command_line');
// Similar to nodes --expose-internals flag, this exposes _linkedBinding so
// that tests can call it to get access to some test only bindings
if (hasSwitch('unsafely-expose-electron-internals-for-testing')) {
preloadProcess._linkedBinding = process._linkedBinding;
}
return preloadProcess;
}
// This is the `require` function that will be visible to the preload script
function preloadRequire (context: PreloadContext, module: string) {
if (context.loadedModules.has(module)) {
return context.loadedModules.get(module);
}
if (context.loadableModules.has(module)) {
const loadedModule = context.loadableModules.get(module)!();
context.loadedModules.set(module, loadedModule);
return loadedModule;
}
throw new Error(`module not found: ${module}`);
}
// Wrap the script into a function executed in global scope. It won't have
// access to the current scope, so we'll expose a few objects as arguments:
//
// - `require`: The `preloadRequire` function
// - `process`: The `preloadProcess` object
// - `Buffer`: Shim of `Buffer` implementation
// - `global`: The window object, which is aliased to `global` by webpack.
function runPreloadScript (context: PreloadContext, preloadSrc: string) {
const globalVariables = [];
const fnParameters = [];
for (const [key, value] of Object.entries(context.exposeGlobals)) {
globalVariables.push(key);
fnParameters.push(value);
}
const preloadWrapperSrc = `(function(require, process, exports, module, ${globalVariables.join(', ')}) {
${preloadSrc}
})`;
// eval in window scope
const preloadFn = context.createPreloadScript(preloadWrapperSrc);
const exports = {};
preloadFn(preloadRequire.bind(null, context), context.process, exports, { exports }, ...fnParameters);
}
/**
* Execute preload scripts within a sandboxed process.
*/
export function executeSandboxedPreloadScripts (context: PreloadContext, preloadScripts: ElectronInternal.PreloadScript[]) {
for (const { filePath, contents, error } of preloadScripts) {
try {
if (contents) {
runPreloadScript(context, contents);
} else if (error) {
throw error;
}
} catch (error) {
console.error(`Unable to load preload script: ${filePath}`);
console.error(error);
const { ipcRendererInternal } = getIpcRendererLazy();
ipcRendererInternal.send(IPC_MESSAGES.BROWSER_PRELOAD_ERROR, filePath, error);
}
}
}

View File

@@ -44,6 +44,10 @@ const main = async () => {
{
name: 'utility_bundle_deps',
config: 'webpack.config.utility.js'
},
{
name: 'preload_realm_bundle_deps',
config: 'webpack.config.preload_realm.js'
}
];

View File

@@ -13,11 +13,18 @@
#include "gin/data_object_builder.h"
#include "gin/handle.h"
#include "gin/object_template_builder.h"
#include "shell/browser/api/electron_api_service_worker_main.h"
#include "shell/browser/electron_browser_context.h"
#include "shell/browser/javascript_environment.h"
#include "shell/common/gin_converters/gurl_converter.h"
#include "shell/common/gin_converters/service_worker_converter.h"
#include "shell/common/gin_converters/value_converter.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/promise.h"
#include "shell/common/node_util.h"
using ServiceWorkerStatus =
content::ServiceWorkerRunningInfo::ServiceWorkerVersionStatus;
namespace electron::api {
@@ -72,8 +79,8 @@ gin::WrapperInfo ServiceWorkerContext::kWrapperInfo = {gin::kEmbedderNativeGin};
ServiceWorkerContext::ServiceWorkerContext(
v8::Isolate* isolate,
ElectronBrowserContext* browser_context) {
service_worker_context_ =
browser_context->GetDefaultStoragePartition()->GetServiceWorkerContext();
storage_partition_ = browser_context->GetDefaultStoragePartition();
service_worker_context_ = storage_partition_->GetServiceWorkerContext();
service_worker_context_->AddObserver(this);
}
@@ -81,6 +88,23 @@ ServiceWorkerContext::~ServiceWorkerContext() {
service_worker_context_->RemoveObserver(this);
}
void ServiceWorkerContext::OnRunningStatusChanged(
int64_t version_id,
blink::EmbeddedWorkerStatus running_status) {
ServiceWorkerMain* worker =
ServiceWorkerMain::FromVersionID(version_id, storage_partition_);
if (worker)
worker->OnRunningStatusChanged();
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
v8::HandleScope scope(isolate);
EmitWithoutEvent("running-status-changed",
gin::DataObjectBuilder(isolate)
.Set("versionId", version_id)
.Set("runningStatus", running_status)
.Build());
}
void ServiceWorkerContext::OnReportConsoleMessage(
int64_t version_id,
const GURL& scope,
@@ -105,6 +129,32 @@ void ServiceWorkerContext::OnRegistrationCompleted(const GURL& scope) {
gin::DataObjectBuilder(isolate).Set("scope", scope).Build());
}
void ServiceWorkerContext::OnVersionRedundant(int64_t version_id,
const GURL& scope) {
ServiceWorkerMain* worker =
ServiceWorkerMain::FromVersionID(version_id, storage_partition_);
if (worker)
worker->OnVersionRedundant();
}
void ServiceWorkerContext::OnVersionStartingRunning(int64_t version_id) {
OnRunningStatusChanged(version_id, blink::EmbeddedWorkerStatus::kStarting);
}
void ServiceWorkerContext::OnVersionStartedRunning(
int64_t version_id,
const content::ServiceWorkerRunningInfo& running_info) {
OnRunningStatusChanged(version_id, blink::EmbeddedWorkerStatus::kRunning);
}
void ServiceWorkerContext::OnVersionStoppingRunning(int64_t version_id) {
OnRunningStatusChanged(version_id, blink::EmbeddedWorkerStatus::kStopping);
}
void ServiceWorkerContext::OnVersionStoppedRunning(int64_t version_id) {
OnRunningStatusChanged(version_id, blink::EmbeddedWorkerStatus::kStopped);
}
void ServiceWorkerContext::OnDestruct(content::ServiceWorkerContext* context) {
if (context == service_worker_context_) {
delete this;
@@ -124,7 +174,7 @@ v8::Local<v8::Value> ServiceWorkerContext::GetAllRunningWorkerInfo(
return builder.Build();
}
v8::Local<v8::Value> ServiceWorkerContext::GetWorkerInfoFromID(
v8::Local<v8::Value> ServiceWorkerContext::GetInfoFromVersionID(
gin_helper::ErrorThrower thrower,
int64_t version_id) {
const base::flat_map<int64_t, content::ServiceWorkerRunningInfo>& info_map =
@@ -138,6 +188,87 @@ v8::Local<v8::Value> ServiceWorkerContext::GetWorkerInfoFromID(
std::move(iter->second));
}
v8::Local<v8::Value> ServiceWorkerContext::GetFromVersionID(
gin_helper::ErrorThrower thrower,
int64_t version_id) {
util::EmitWarning(thrower.isolate(),
"The session.serviceWorkers.getFromVersionID API is "
"deprecated, use "
"session.serviceWorkers.getInfoFromVersionID instead.",
"ServiceWorkersDeprecateGetFromVersionID");
return GetInfoFromVersionID(thrower, version_id);
}
v8::Local<v8::Value> ServiceWorkerContext::GetWorkerFromVersionID(
v8::Isolate* isolate,
int64_t version_id) {
return ServiceWorkerMain::From(isolate, service_worker_context_,
storage_partition_, version_id)
.ToV8();
}
gin::Handle<ServiceWorkerMain>
ServiceWorkerContext::GetWorkerFromVersionIDIfExists(v8::Isolate* isolate,
int64_t version_id) {
ServiceWorkerMain* worker =
ServiceWorkerMain::FromVersionID(version_id, storage_partition_);
if (!worker)
return gin::Handle<ServiceWorkerMain>();
return gin::CreateHandle(isolate, worker);
}
v8::Local<v8::Promise> ServiceWorkerContext::StartWorkerForScope(
v8::Isolate* isolate,
GURL scope) {
auto shared_promise =
std::make_shared<gin_helper::Promise<v8::Local<v8::Value>>>(isolate);
v8::Local<v8::Promise> handle = shared_promise->GetHandle();
blink::StorageKey storage_key =
blink::StorageKey::CreateFirstParty(url::Origin::Create(scope));
service_worker_context_->StartWorkerForScope(
scope, storage_key,
base::BindOnce(&ServiceWorkerContext::DidStartWorkerForScope,
weak_ptr_factory_.GetWeakPtr(), shared_promise),
base::BindOnce(&ServiceWorkerContext::DidFailToStartWorkerForScope,
weak_ptr_factory_.GetWeakPtr(), shared_promise));
return handle;
}
void ServiceWorkerContext::DidStartWorkerForScope(
std::shared_ptr<gin_helper::Promise<v8::Local<v8::Value>>> shared_promise,
int64_t version_id,
int process_id,
int thread_id) {
v8::Isolate* isolate = shared_promise->isolate();
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Value> service_worker_main =
GetWorkerFromVersionID(isolate, version_id);
shared_promise->Resolve(service_worker_main);
shared_promise.reset();
}
void ServiceWorkerContext::DidFailToStartWorkerForScope(
std::shared_ptr<gin_helper::Promise<v8::Local<v8::Value>>> shared_promise,
blink::ServiceWorkerStatusCode status_code) {
shared_promise->RejectWithErrorMessage("Failed to start service worker.");
shared_promise.reset();
}
v8::Local<v8::Promise> ServiceWorkerContext::StopAllWorkers(
v8::Isolate* isolate) {
auto promise = gin_helper::Promise<void>(isolate);
v8::Local<v8::Promise> handle = promise.GetHandle();
service_worker_context_->StopAllServiceWorkers(base::BindOnce(
[](gin_helper::Promise<void> promise) { promise.Resolve(); },
std::move(promise)));
return handle;
}
// static
gin::Handle<ServiceWorkerContext> ServiceWorkerContext::Create(
v8::Isolate* isolate,
@@ -153,8 +284,16 @@ gin::ObjectTemplateBuilder ServiceWorkerContext::GetObjectTemplateBuilder(
ServiceWorkerContext>::GetObjectTemplateBuilder(isolate)
.SetMethod("getAllRunning",
&ServiceWorkerContext::GetAllRunningWorkerInfo)
.SetMethod("getFromVersionID",
&ServiceWorkerContext::GetWorkerInfoFromID);
.SetMethod("getFromVersionID", &ServiceWorkerContext::GetFromVersionID)
.SetMethod("getInfoFromVersionID",
&ServiceWorkerContext::GetInfoFromVersionID)
.SetMethod("getWorkerFromVersionID",
&ServiceWorkerContext::GetWorkerFromVersionID)
.SetMethod("_getWorkerFromVersionIDIfExists",
&ServiceWorkerContext::GetWorkerFromVersionIDIfExists)
.SetMethod("startWorkerForScope",
&ServiceWorkerContext::StartWorkerForScope)
.SetMethod("_stopAllWorkers", &ServiceWorkerContext::StopAllWorkers);
}
const char* ServiceWorkerContext::GetTypeName() {

View File

@@ -10,18 +10,30 @@
#include "content/public/browser/service_worker_context_observer.h"
#include "gin/wrappable.h"
#include "shell/browser/event_emitter_mixin.h"
#include "third_party/blink/public/common/service_worker/embedded_worker_status.h"
namespace content {
class StoragePartition;
}
namespace gin {
template <typename T>
class Handle;
} // namespace gin
namespace gin_helper {
template <typename T>
class Promise;
} // namespace gin_helper
namespace electron {
class ElectronBrowserContext;
namespace api {
class ServiceWorkerMain;
class ServiceWorkerContext final
: public gin::Wrappable<ServiceWorkerContext>,
public gin_helper::EventEmitterMixin<ServiceWorkerContext>,
@@ -32,14 +44,39 @@ class ServiceWorkerContext final
ElectronBrowserContext* browser_context);
v8::Local<v8::Value> GetAllRunningWorkerInfo(v8::Isolate* isolate);
v8::Local<v8::Value> GetWorkerInfoFromID(gin_helper::ErrorThrower thrower,
int64_t version_id);
v8::Local<v8::Value> GetInfoFromVersionID(gin_helper::ErrorThrower thrower,
int64_t version_id);
v8::Local<v8::Value> GetFromVersionID(gin_helper::ErrorThrower thrower,
int64_t version_id);
v8::Local<v8::Value> GetWorkerFromVersionID(v8::Isolate* isolate,
int64_t version_id);
gin::Handle<ServiceWorkerMain> GetWorkerFromVersionIDIfExists(
v8::Isolate* isolate,
int64_t version_id);
v8::Local<v8::Promise> StartWorkerForScope(v8::Isolate* isolate, GURL scope);
void DidStartWorkerForScope(
std::shared_ptr<gin_helper::Promise<v8::Local<v8::Value>>> shared_promise,
int64_t version_id,
int process_id,
int thread_id);
void DidFailToStartWorkerForScope(
std::shared_ptr<gin_helper::Promise<v8::Local<v8::Value>>> shared_promise,
blink::ServiceWorkerStatusCode status_code);
void StopWorkersForScope(GURL scope);
v8::Local<v8::Promise> StopAllWorkers(v8::Isolate* isolate);
// content::ServiceWorkerContextObserver
void OnReportConsoleMessage(int64_t version_id,
const GURL& scope,
const content::ConsoleMessage& message) override;
void OnRegistrationCompleted(const GURL& scope) override;
void OnVersionStartingRunning(int64_t version_id) override;
void OnVersionStartedRunning(
int64_t version_id,
const content::ServiceWorkerRunningInfo& running_info) override;
void OnVersionStoppingRunning(int64_t version_id) override;
void OnVersionStoppedRunning(int64_t version_id) override;
void OnVersionRedundant(int64_t version_id, const GURL& scope) override;
void OnDestruct(content::ServiceWorkerContext* context) override;
// gin::Wrappable
@@ -58,8 +95,15 @@ class ServiceWorkerContext final
~ServiceWorkerContext() override;
private:
void OnRunningStatusChanged(int64_t version_id,
blink::EmbeddedWorkerStatus running_status);
raw_ptr<content::ServiceWorkerContext> service_worker_context_;
// Service worker registration and versions are unique to a storage partition.
// Keep a reference to the storage partition to be used for lookups.
raw_ptr<content::StoragePartition> storage_partition_;
base::WeakPtrFactory<ServiceWorkerContext> weak_ptr_factory_{this};
};

View File

@@ -0,0 +1,319 @@
// 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/api/electron_api_service_worker_main.h"
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
#include "base/logging.h"
#include "base/no_destructor.h"
#include "content/browser/service_worker/service_worker_context_wrapper.h" // nogncheck
#include "content/browser/service_worker/service_worker_version.h" // nogncheck
#include "electron/shell/common/api/api.mojom.h"
#include "gin/handle.h"
#include "gin/object_template_builder.h"
#include "services/service_manager/public/cpp/interface_provider.h"
#include "shell/browser/api/message_port.h"
#include "shell/browser/browser.h"
#include "shell/browser/javascript_environment.h"
#include "shell/common/gin_converters/blink_converter.h"
#include "shell/common/gin_converters/gurl_converter.h"
#include "shell/common/gin_converters/value_converter.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/error_thrower.h"
#include "shell/common/gin_helper/object_template_builder.h"
#include "shell/common/gin_helper/promise.h"
#include "shell/common/node_includes.h"
#include "shell/common/v8_util.h"
namespace {
// Use private API to get the live version of the service worker. This will
// exist while in starting, stopping, or stopped running status.
content::ServiceWorkerVersion* GetLiveVersion(
content::ServiceWorkerContext* service_worker_context,
int64_t version_id) {
auto* wrapper = static_cast<content::ServiceWorkerContextWrapper*>(
service_worker_context);
return wrapper->GetLiveVersion(version_id);
}
// Get a public ServiceWorkerVersionBaseInfo object directly from the service
// worker.
std::optional<content::ServiceWorkerVersionBaseInfo> GetLiveVersionInfo(
content::ServiceWorkerContext* service_worker_context,
int64_t version_id) {
auto* version = GetLiveVersion(service_worker_context, version_id);
if (version) {
return version->GetInfo();
}
return std::nullopt;
}
} // namespace
namespace electron::api {
// ServiceWorkerKey -> ServiceWorkerMain*
typedef std::unordered_map<ServiceWorkerKey,
ServiceWorkerMain*,
ServiceWorkerKey::Hasher>
VersionIdMap;
VersionIdMap& GetVersionIdMap() {
static base::NoDestructor<VersionIdMap> instance;
return *instance;
}
ServiceWorkerMain* FromServiceWorkerKey(const ServiceWorkerKey& key) {
VersionIdMap& version_map = GetVersionIdMap();
auto iter = version_map.find(key);
auto* service_worker = iter == version_map.end() ? nullptr : iter->second;
return service_worker;
}
// static
ServiceWorkerMain* ServiceWorkerMain::FromVersionID(
int64_t version_id,
const content::StoragePartition* storage_partition) {
ServiceWorkerKey key(version_id, storage_partition);
return FromServiceWorkerKey(key);
}
gin::WrapperInfo ServiceWorkerMain::kWrapperInfo = {gin::kEmbedderNativeGin};
ServiceWorkerMain::ServiceWorkerMain(content::ServiceWorkerContext* sw_context,
int64_t version_id,
const ServiceWorkerKey& key)
: version_id_(version_id), key_(key), service_worker_context_(sw_context) {
GetVersionIdMap().emplace(key_, this);
InvalidateVersionInfo();
}
ServiceWorkerMain::~ServiceWorkerMain() {
Destroy();
}
void ServiceWorkerMain::Destroy() {
version_destroyed_ = true;
InvalidateVersionInfo();
GetVersionIdMap().erase(key_);
Unpin();
}
mojom::ElectronRenderer* ServiceWorkerMain::GetRendererApi() {
if (!remote_.is_bound()) {
if (!service_worker_context_->IsLiveRunningServiceWorker(version_id_)) {
return nullptr;
}
service_worker_context_->GetRemoteAssociatedInterfaces(version_id_)
.GetInterface(&remote_);
}
return remote_.get();
}
void ServiceWorkerMain::Send(v8::Isolate* isolate,
bool internal,
const std::string& channel,
v8::Local<v8::Value> args) {
blink::CloneableMessage message;
if (!gin::ConvertFromV8(isolate, args, &message)) {
isolate->ThrowException(v8::Exception::Error(
gin::StringToV8(isolate, "Failed to serialize arguments")));
return;
}
auto* renderer_api_remote = GetRendererApi();
if (!renderer_api_remote) {
return;
}
renderer_api_remote->Message(internal, channel, std::move(message));
}
void ServiceWorkerMain::InvalidateVersionInfo() {
if (version_info_ != nullptr) {
version_info_.reset();
}
if (version_destroyed_)
return;
auto version_info = GetLiveVersionInfo(service_worker_context_, version_id_);
if (version_info) {
version_info_ =
std::make_unique<content::ServiceWorkerVersionBaseInfo>(*version_info);
} else {
// When ServiceWorkerContextCore::RemoveLiveVersion is called, it posts a
// task to notify that the service worker has stopped. At this point, the
// live version will no longer exist.
Destroy();
}
}
void ServiceWorkerMain::OnRunningStatusChanged() {
InvalidateVersionInfo();
// Disconnect remote when content::ServiceWorkerHost has terminated.
if (remote_.is_bound() &&
!service_worker_context_->IsLiveStartingServiceWorker(version_id_) &&
!service_worker_context_->IsLiveRunningServiceWorker(version_id_)) {
remote_.reset();
}
}
void ServiceWorkerMain::OnVersionRedundant() {
// Redundant service workers have become either unregistered or replaced.
// A new ServiceWorkerMain will need to be created.
Destroy();
}
bool ServiceWorkerMain::IsDestroyed() const {
return version_destroyed_;
}
const blink::StorageKey ServiceWorkerMain::GetStorageKey() {
GURL scope = version_info()->scope;
return blink::StorageKey::CreateFirstParty(url::Origin::Create(scope));
}
gin_helper::Dictionary ServiceWorkerMain::StartExternalRequest(
v8::Isolate* isolate,
bool has_timeout) {
auto details = gin_helper::Dictionary::CreateEmpty(isolate);
if (version_destroyed_) {
isolate->ThrowException(v8::Exception::TypeError(
gin::StringToV8(isolate, "ServiceWorkerMain is destroyed")));
return details;
}
auto request_uuid = base::Uuid::GenerateRandomV4();
auto timeout_type =
has_timeout
? content::ServiceWorkerExternalRequestTimeoutType::kDefault
: content::ServiceWorkerExternalRequestTimeoutType::kDoesNotTimeout;
content::ServiceWorkerExternalRequestResult start_result =
service_worker_context_->StartingExternalRequest(
version_id_, timeout_type, request_uuid);
details.Set("id", request_uuid.AsLowercaseString());
details.Set("ok",
start_result == content::ServiceWorkerExternalRequestResult::kOk);
return details;
}
void ServiceWorkerMain::FinishExternalRequest(v8::Isolate* isolate,
std::string uuid) {
if (version_destroyed_) {
isolate->ThrowException(v8::Exception::TypeError(
gin::StringToV8(isolate, "ServiceWorkerMain is destroyed")));
return;
}
base::Uuid request_uuid = base::Uuid::ParseLowercase(uuid);
if (!request_uuid.is_valid()) {
isolate->ThrowException(v8::Exception::TypeError(
gin::StringToV8(isolate, "Invalid external request UUID")));
return;
}
service_worker_context_->FinishedExternalRequest(version_id_, request_uuid);
}
size_t ServiceWorkerMain::CountExternalRequests() {
auto& storage_key = GetStorageKey();
return service_worker_context_->CountExternalRequestsForTest(storage_key);
}
int64_t ServiceWorkerMain::VersionID() const {
return version_id_;
}
GURL ServiceWorkerMain::ScopeURL() const {
if (version_destroyed_)
return GURL::EmptyGURL();
return version_info()->scope;
}
// static
gin::Handle<ServiceWorkerMain> ServiceWorkerMain::New(v8::Isolate* isolate) {
return gin::Handle<ServiceWorkerMain>();
}
// static
gin::Handle<ServiceWorkerMain> ServiceWorkerMain::From(
v8::Isolate* isolate,
content::ServiceWorkerContext* sw_context,
const content::StoragePartition* storage_partition,
int64_t version_id) {
ServiceWorkerKey service_worker_key(version_id, storage_partition);
auto* service_worker = FromServiceWorkerKey(service_worker_key);
if (service_worker)
return gin::CreateHandle(isolate, service_worker);
// Ensure ServiceWorkerVersion exists and is not redundant (pending deletion)
auto* live_version = GetLiveVersion(sw_context, version_id);
if (!live_version || live_version->is_redundant()) {
return gin::Handle<ServiceWorkerMain>();
}
auto handle = gin::CreateHandle(
isolate,
new ServiceWorkerMain(sw_context, version_id, service_worker_key));
// Prevent garbage collection of worker until it has been deleted internally.
handle->Pin(isolate);
return handle;
}
// static
void ServiceWorkerMain::FillObjectTemplate(
v8::Isolate* isolate,
v8::Local<v8::ObjectTemplate> templ) {
gin_helper::ObjectTemplateBuilder(isolate, templ)
.SetMethod("_send", &ServiceWorkerMain::Send)
.SetMethod("isDestroyed", &ServiceWorkerMain::IsDestroyed)
.SetMethod("_startExternalRequest",
&ServiceWorkerMain::StartExternalRequest)
.SetMethod("_finishExternalRequest",
&ServiceWorkerMain::FinishExternalRequest)
.SetMethod("_countExternalRequests",
&ServiceWorkerMain::CountExternalRequests)
.SetProperty("versionId", &ServiceWorkerMain::VersionID)
.SetProperty("scope", &ServiceWorkerMain::ScopeURL)
.Build();
}
const char* ServiceWorkerMain::GetTypeName() {
return GetClassName();
}
} // namespace electron::api
namespace {
using electron::api::ServiceWorkerMain;
void Initialize(v8::Local<v8::Object> exports,
v8::Local<v8::Value> unused,
v8::Local<v8::Context> context,
void* priv) {
v8::Isolate* isolate = context->GetIsolate();
gin_helper::Dictionary dict(isolate, exports);
dict.Set("ServiceWorkerMain", ServiceWorkerMain::GetConstructor(context));
}
} // namespace
NODE_LINKED_BINDING_CONTEXT_AWARE(electron_browser_service_worker_main,
Initialize)

View File

@@ -0,0 +1,174 @@
// 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_ELECTRON_API_SERVICE_WORKER_MAIN_H_
#define ELECTRON_SHELL_BROWSER_API_ELECTRON_API_SERVICE_WORKER_MAIN_H_
#include <optional>
#include <string>
#include <vector>
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/process/process.h"
#include "content/public/browser/global_routing_id.h"
#include "content/public/browser/service_worker_context.h"
#include "content/public/browser/service_worker_version_base_info.h"
#include "gin/wrappable.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/remote.h"
#include "shell/browser/event_emitter_mixin.h"
#include "shell/common/api/api.mojom.h"
#include "shell/common/gin_helper/constructible.h"
#include "shell/common/gin_helper/pinnable.h"
#include "third_party/blink/public/common/service_worker/embedded_worker_status.h"
class GURL;
namespace content {
class StoragePartition;
}
namespace gin {
class Arguments;
} // namespace gin
namespace gin_helper {
class Dictionary;
template <typename T>
class Handle;
template <typename T>
class Promise;
} // namespace gin_helper
namespace electron::api {
// Key to uniquely identify a ServiceWorkerMain by its Version ID within the
// associated StoragePartition.
struct ServiceWorkerKey {
int64_t version_id;
raw_ptr<const content::StoragePartition> storage_partition;
ServiceWorkerKey(int64_t id, const content::StoragePartition* partition)
: version_id(id), storage_partition(partition) {}
bool operator<(const ServiceWorkerKey& other) const {
return std::tie(version_id, storage_partition) <
std::tie(other.version_id, other.storage_partition);
}
bool operator==(const ServiceWorkerKey& other) const {
return version_id == other.version_id &&
storage_partition == other.storage_partition;
}
struct Hasher {
std::size_t operator()(const ServiceWorkerKey& key) const {
return std::hash<const content::StoragePartition*>()(
key.storage_partition) ^
std::hash<int64_t>()(key.version_id);
}
};
};
// Creates a wrapper to align with the lifecycle of the non-public
// content::ServiceWorkerVersion. Object instances are pinned for the lifetime
// of the underlying SW such that registered IPC handlers continue to dispatch.
//
// Instances are uniquely identified by pairing their version ID and the
// StoragePartition in which they're registered. In Electron, this is always
// the default StoragePartition for the associated BrowserContext.
class ServiceWorkerMain final
: public gin::Wrappable<ServiceWorkerMain>,
public gin_helper::EventEmitterMixin<ServiceWorkerMain>,
public gin_helper::Pinnable<ServiceWorkerMain>,
public gin_helper::Constructible<ServiceWorkerMain> {
public:
// Create a new ServiceWorkerMain and return the V8 wrapper of it.
static gin::Handle<ServiceWorkerMain> New(v8::Isolate* isolate);
static gin::Handle<ServiceWorkerMain> From(
v8::Isolate* isolate,
content::ServiceWorkerContext* sw_context,
const content::StoragePartition* storage_partition,
int64_t version_id);
static ServiceWorkerMain* FromVersionID(
int64_t version_id,
const content::StoragePartition* storage_partition);
// gin_helper::Constructible
static void FillObjectTemplate(v8::Isolate*, v8::Local<v8::ObjectTemplate>);
static const char* GetClassName() { return "ServiceWorkerMain"; }
// gin::Wrappable
static gin::WrapperInfo kWrapperInfo;
const char* GetTypeName() override;
// disable copy
ServiceWorkerMain(const ServiceWorkerMain&) = delete;
ServiceWorkerMain& operator=(const ServiceWorkerMain&) = delete;
void OnRunningStatusChanged();
void OnVersionRedundant();
protected:
explicit ServiceWorkerMain(content::ServiceWorkerContext* sw_context,
int64_t version_id,
const ServiceWorkerKey& key);
~ServiceWorkerMain() override;
private:
void Destroy();
const blink::StorageKey GetStorageKey();
// Increments external requests for the service worker to keep it alive.
gin_helper::Dictionary StartExternalRequest(v8::Isolate* isolate,
bool has_timeout);
void FinishExternalRequest(v8::Isolate* isolate, std::string uuid);
size_t CountExternalRequests();
// Get or create a Mojo connection to the renderer process.
mojom::ElectronRenderer* GetRendererApi();
// Send a message to the renderer process.
void Send(v8::Isolate* isolate,
bool internal,
const std::string& channel,
v8::Local<v8::Value> args);
void InvalidateVersionInfo();
const content::ServiceWorkerVersionBaseInfo* version_info() const {
return version_info_.get();
}
bool IsDestroyed() const;
int64_t VersionID() const;
GURL ScopeURL() const;
// Version ID unique only to the StoragePartition.
int64_t version_id_;
// Unique identifier pairing the Version ID and StoragePartition.
ServiceWorkerKey key_;
// Whether the Service Worker version has been destroyed.
bool version_destroyed_ = false;
// Store copy of version info so it's accessible when not running.
std::unique_ptr<content::ServiceWorkerVersionBaseInfo> version_info_;
raw_ptr<content::ServiceWorkerContext> service_worker_context_;
mojo::AssociatedRemote<mojom::ElectronRenderer> remote_;
std::unique_ptr<gin_helper::Promise<void>> start_worker_promise_;
base::WeakPtrFactory<ServiceWorkerMain> weak_factory_{this};
};
} // namespace electron::api
#endif // ELECTRON_SHELL_BROWSER_API_ELECTRON_API_SERVICE_WORKER_MAIN_H_

View File

@@ -1064,16 +1064,72 @@ void Session::CreateInterruptedDownload(const gin_helper::Dictionary& options) {
base::Time::FromSecondsSinceUnixEpoch(start_time)));
}
void Session::SetPreloads(const std::vector<base::FilePath>& preloads) {
std::string Session::RegisterPreloadScript(
gin_helper::ErrorThrower thrower,
const PreloadScript& new_preload_script) {
auto* prefs = SessionPreferences::FromBrowserContext(browser_context());
DCHECK(prefs);
prefs->set_preloads(preloads);
auto& preload_scripts = prefs->preload_scripts();
auto it = std::find_if(preload_scripts.begin(), preload_scripts.end(),
[&new_preload_script](const PreloadScript& script) {
return script.id == new_preload_script.id;
});
if (it != preload_scripts.end()) {
thrower.ThrowError(base::StringPrintf(
"Cannot register preload script with existing ID '%s'",
new_preload_script.id.c_str()));
return "";
}
if (!new_preload_script.file_path.IsAbsolute()) {
// Deprecated preload scripts logged error without throwing.
if (new_preload_script.deprecated) {
LOG(ERROR) << "preload script must have absolute path: "
<< new_preload_script.file_path;
} else {
thrower.ThrowError(
base::StringPrintf("Preload script must have absolute path: %s",
new_preload_script.file_path.value().c_str()));
return "";
}
}
preload_scripts.push_back(new_preload_script);
return new_preload_script.id;
}
std::vector<base::FilePath> Session::GetPreloads() const {
void Session::UnregisterPreloadScript(gin_helper::ErrorThrower thrower,
const std::string& script_id) {
auto* prefs = SessionPreferences::FromBrowserContext(browser_context());
DCHECK(prefs);
return prefs->preloads();
auto& preload_scripts = prefs->preload_scripts();
// Find the preload script by its ID
auto it = std::find_if(preload_scripts.begin(), preload_scripts.end(),
[&script_id](const PreloadScript& script) {
return script.id == script_id;
});
// If the script is found, erase it from the vector
if (it != preload_scripts.end()) {
preload_scripts.erase(it);
return;
}
// If the script is not found, throw an error
thrower.ThrowError(base::StringPrintf(
"Cannot unregister preload script with non-existing ID '%s'",
script_id.c_str()));
}
std::vector<PreloadScript> Session::GetPreloadScripts() const {
auto* prefs = SessionPreferences::FromBrowserContext(browser_context());
DCHECK(prefs);
return prefs->preload_scripts();
}
/**
@@ -1705,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;
@@ -1799,8 +1861,9 @@ void Session::FillObjectTemplate(v8::Isolate* isolate,
.SetMethod("downloadURL", &Session::DownloadURL)
.SetMethod("createInterruptedDownload",
&Session::CreateInterruptedDownload)
.SetMethod("setPreloads", &Session::SetPreloads)
.SetMethod("getPreloads", &Session::GetPreloads)
.SetMethod("registerPreloadScript", &Session::RegisterPreloadScript)
.SetMethod("unregisterPreloadScript", &Session::UnregisterPreloadScript)
.SetMethod("getPreloadScripts", &Session::GetPreloadScripts)
.SetMethod("getSharedDictionaryUsageInfo",
&Session::GetSharedDictionaryUsageInfo)
.SetMethod("getSharedDictionaryInfo", &Session::GetSharedDictionaryInfo)

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"
@@ -57,6 +58,7 @@ class ProxyConfig;
namespace electron {
class ElectronBrowserContext;
struct PreloadScript;
namespace api {
@@ -65,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
@@ -138,8 +141,11 @@ class Session final : public gin::Wrappable<Session>,
const std::string& uuid);
void DownloadURL(const GURL& url, gin::Arguments* args);
void CreateInterruptedDownload(const gin_helper::Dictionary& options);
void SetPreloads(const std::vector<base::FilePath>& preloads);
std::vector<base::FilePath> GetPreloads() const;
std::string RegisterPreloadScript(gin_helper::ErrorThrower thrower,
const PreloadScript& new_preload_script);
void UnregisterPreloadScript(gin_helper::ErrorThrower thrower,
const std::string& script_id);
std::vector<PreloadScript> GetPreloadScripts() const;
v8::Local<v8::Promise> GetSharedDictionaryInfo(
const gin_helper::Dictionary& options);
v8::Local<v8::Promise> GetSharedDictionaryUsageInfo();

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());
@@ -3706,16 +3648,15 @@ void WebContents::DoGetZoomLevel(
std::move(callback).Run(GetZoomLevel());
}
std::vector<base::FilePath> WebContents::GetPreloadPaths() const {
auto result = SessionPreferences::GetValidPreloads(GetBrowserContext());
std::optional<PreloadScript> WebContents::GetPreloadScript() const {
if (auto* web_preferences = WebContentsPreferences::From(web_contents())) {
if (auto preload = web_preferences->GetPreloadPath()) {
result.emplace_back(*preload);
auto preload_script = PreloadScript{
"", PreloadScript::ScriptType::kWebFrame, preload.value()};
return preload_script;
}
}
return result;
return std::nullopt;
}
v8::Local<v8::Value> WebContents::GetLastWebPreferences(
@@ -4469,7 +4410,7 @@ void WebContents::FillObjectTemplate(v8::Isolate* isolate,
.SetMethod("setZoomFactor", &WebContents::SetZoomFactor)
.SetMethod("getZoomFactor", &WebContents::GetZoomFactor)
.SetMethod("getType", &WebContents::type)
.SetMethod("_getPreloadPaths", &WebContents::GetPreloadPaths)
.SetMethod("_getPreloadScript", &WebContents::GetPreloadScript)
.SetMethod("getLastWebPreferences", &WebContents::GetLastWebPreferences)
.SetMethod("getOwnerBrowserWindow", &WebContents::GetOwnerBrowserWindow)
.SetMethod("inspectServiceWorker", &WebContents::InspectServiceWorker)

View File

@@ -40,6 +40,7 @@
#include "shell/browser/event_emitter_mixin.h"
#include "shell/browser/extended_web_contents_observer.h"
#include "shell/browser/osr/osr_paint_event.h"
#include "shell/browser/preload_script.h"
#include "shell/browser/ui/inspectable_web_contents_delegate.h"
#include "shell/browser/ui/inspectable_web_contents_view_delegate.h"
#include "shell/common/gin_helper/cleaned_up_at_exit.h"
@@ -337,8 +338,8 @@ class WebContents final : public ExclusiveAccessContext,
const std::string& features,
const scoped_refptr<network::ResourceRequestBody>& body);
// Returns the preload script path of current WebContents.
std::vector<base::FilePath> GetPreloadPaths() const;
// Returns the preload script of current WebContents.
std::optional<PreloadScript> GetPreloadScript() const;
// Returns the web preferences of current WebContents.
v8::Local<v8::Value> GetLastWebPreferences(v8::Isolate* isolate) const;

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"
@@ -578,6 +579,18 @@ void ElectronBrowserClient::AppendExtraCommandLineSwitches(
web_preferences->AppendCommandLineSwitches(
command_line, IsRendererSubFrame(process_id));
}
// Service worker processes should only run preloads if one has been
// registered prior to startup.
auto* render_process_host = content::RenderProcessHost::FromID(process_id);
if (render_process_host) {
auto* browser_context = render_process_host->GetBrowserContext();
auto* session_prefs =
SessionPreferences::FromBrowserContext(browser_context);
if (session_prefs->HasServiceWorkerPreloadScript()) {
command_line->AppendSwitch(switches::kServiceWorkerPreload);
}
}
}
}
@@ -1405,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,104 @@
// 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_PRELOAD_SCRIPT_H_
#define ELECTRON_SHELL_BROWSER_PRELOAD_SCRIPT_H_
#include <string_view>
#include "base/containers/fixed_flat_map.h"
#include "base/files/file_path.h"
#include "base/uuid.h"
#include "gin/converter.h"
#include "shell/common/gin_converters/file_path_converter.h"
#include "shell/common/gin_helper/dictionary.h"
namespace electron {
struct PreloadScript {
enum class ScriptType { kWebFrame, kServiceWorker };
std::string id;
ScriptType script_type;
base::FilePath file_path;
// If set, use the deprecated validation behavior of Session.setPreloads
bool deprecated = false;
};
} // namespace electron
namespace gin {
using electron::PreloadScript;
template <>
struct Converter<PreloadScript::ScriptType> {
static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
const PreloadScript::ScriptType& in) {
using Val = PreloadScript::ScriptType;
static constexpr auto Lookup =
base::MakeFixedFlatMap<Val, std::string_view>({
{Val::kWebFrame, "frame"},
{Val::kServiceWorker, "service-worker"},
});
return StringToV8(isolate, Lookup.at(in));
}
static bool FromV8(v8::Isolate* isolate,
v8::Local<v8::Value> val,
PreloadScript::ScriptType* out) {
using Val = PreloadScript::ScriptType;
static constexpr auto Lookup =
base::MakeFixedFlatMap<std::string_view, Val>({
{"frame", Val::kWebFrame},
{"service-worker", Val::kServiceWorker},
});
return FromV8WithLookup(isolate, val, Lookup, out);
}
};
template <>
struct Converter<PreloadScript> {
static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
const PreloadScript& script) {
gin::Dictionary dict(isolate, v8::Object::New(isolate));
dict.Set("filePath", script.file_path.AsUTF8Unsafe());
dict.Set("id", script.id);
dict.Set("type", script.script_type);
return ConvertToV8(isolate, dict).As<v8::Object>();
}
static bool FromV8(v8::Isolate* isolate,
v8::Local<v8::Value> val,
PreloadScript* out) {
gin_helper::Dictionary options;
if (!ConvertFromV8(isolate, val, &options))
return false;
if (PreloadScript::ScriptType script_type;
options.Get("type", &script_type)) {
out->script_type = script_type;
} else {
return false;
}
if (base::FilePath file_path; options.Get("filePath", &file_path)) {
out->file_path = file_path;
} else {
return false;
}
if (std::string id; options.Get("id", &id)) {
out->id = id;
} else {
out->id = base::Uuid::GenerateRandomV4().AsLowercaseString();
}
if (bool deprecated; options.Get("_deprecated", &deprecated)) {
out->deprecated = deprecated;
}
return true;
}
};
} // namespace gin
#endif // ELECTRON_SHELL_BROWSER_PRELOAD_SCRIPT_H_

View File

@@ -30,22 +30,13 @@ SessionPreferences* SessionPreferences::FromBrowserContext(
return static_cast<SessionPreferences*>(context->GetUserData(&kLocatorKey));
}
// static
std::vector<base::FilePath> SessionPreferences::GetValidPreloads(
content::BrowserContext* context) {
std::vector<base::FilePath> result;
if (auto* self = FromBrowserContext(context)) {
for (const auto& preload : self->preloads()) {
if (preload.IsAbsolute()) {
result.emplace_back(preload);
} else {
LOG(ERROR) << "preload script must have absolute path: " << preload;
}
}
}
return result;
bool SessionPreferences::HasServiceWorkerPreloadScript() {
const auto& preloads = preload_scripts();
auto it = std::find_if(
preloads.begin(), preloads.end(), [](const PreloadScript& script) {
return script.script_type == PreloadScript::ScriptType::kServiceWorker;
});
return it != preloads.end();
}
} // namespace electron

View File

@@ -9,6 +9,7 @@
#include "base/files/file_path.h"
#include "base/supports_user_data.h"
#include "shell/browser/preload_script.h"
namespace content {
class BrowserContext;
@@ -20,17 +21,14 @@ class SessionPreferences : public base::SupportsUserData::Data {
public:
static SessionPreferences* FromBrowserContext(
content::BrowserContext* context);
static std::vector<base::FilePath> GetValidPreloads(
content::BrowserContext* context);
static void CreateForBrowserContext(content::BrowserContext* context);
~SessionPreferences() override;
void set_preloads(const std::vector<base::FilePath>& preloads) {
preloads_ = preloads;
}
const std::vector<base::FilePath>& preloads() const { return preloads_; }
std::vector<PreloadScript>& preload_scripts() { return preload_scripts_; }
bool HasServiceWorkerPreloadScript();
private:
SessionPreferences();
@@ -38,7 +36,7 @@ class SessionPreferences : public base::SupportsUserData::Data {
// The user data key.
static int kLocatorKey;
std::vector<base::FilePath> preloads_;
std::vector<PreloadScript> preload_scripts_;
};
} // namespace electron

View File

@@ -0,0 +1,25 @@
// 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/common/gin_converters/service_worker_converter.h"
#include "base/containers/fixed_flat_map.h"
namespace gin {
// static
v8::Local<v8::Value> Converter<blink::EmbeddedWorkerStatus>::ToV8(
v8::Isolate* isolate,
const blink::EmbeddedWorkerStatus& val) {
static constexpr auto Lookup =
base::MakeFixedFlatMap<blink::EmbeddedWorkerStatus, std::string_view>({
{blink::EmbeddedWorkerStatus::kStarting, "starting"},
{blink::EmbeddedWorkerStatus::kRunning, "running"},
{blink::EmbeddedWorkerStatus::kStopping, "stopping"},
{blink::EmbeddedWorkerStatus::kStopped, "stopped"},
});
return StringToV8(isolate, Lookup.at(val));
}
} // namespace gin

View File

@@ -0,0 +1,21 @@
// 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_COMMON_GIN_CONVERTERS_SERVICE_WORKER_CONVERTER_H_
#define ELECTRON_SHELL_COMMON_GIN_CONVERTERS_SERVICE_WORKER_CONVERTER_H_
#include "gin/converter.h"
#include "third_party/blink/public/common/service_worker/embedded_worker_status.h"
namespace gin {
template <>
struct Converter<blink::EmbeddedWorkerStatus> {
static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
const blink::EmbeddedWorkerStatus& val);
};
} // namespace gin
#endif // ELECTRON_SHELL_COMMON_GIN_CONVERTERS_SERVICE_WORKER_CONVERTER_H_

View File

@@ -33,7 +33,10 @@ struct TranslatorHolder {
};
// Cached JavaScript version of |CallTranslator|.
v8::Persistent<v8::FunctionTemplate> g_call_translator;
// v8::Persistent handles are bound to a specific v8::Isolate. Require
// initializing per-thread to avoid using the wrong isolate from service
// worker preload scripts.
thread_local v8::Persistent<v8::FunctionTemplate> g_call_translator;
void CallTranslator(v8::Local<v8::External> external,
v8::Local<v8::Object> state,

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

@@ -49,39 +49,40 @@
#include "shell/common/crash_keys.h"
#endif
#define ELECTRON_BROWSER_BINDINGS(V) \
V(electron_browser_app) \
V(electron_browser_auto_updater) \
V(electron_browser_content_tracing) \
V(electron_browser_crash_reporter) \
V(electron_browser_desktop_capturer) \
V(electron_browser_dialog) \
V(electron_browser_event_emitter) \
V(electron_browser_global_shortcut) \
V(electron_browser_image_view) \
V(electron_browser_in_app_purchase) \
V(electron_browser_menu) \
V(electron_browser_message_port) \
V(electron_browser_native_theme) \
V(electron_browser_notification) \
V(electron_browser_power_monitor) \
V(electron_browser_power_save_blocker) \
V(electron_browser_protocol) \
V(electron_browser_printing) \
V(electron_browser_push_notifications) \
V(electron_browser_safe_storage) \
V(electron_browser_session) \
V(electron_browser_screen) \
V(electron_browser_system_preferences) \
V(electron_browser_base_window) \
V(electron_browser_tray) \
V(electron_browser_utility_process) \
V(electron_browser_view) \
V(electron_browser_web_contents) \
V(electron_browser_web_contents_view) \
V(electron_browser_web_frame_main) \
V(electron_browser_web_view_manager) \
V(electron_browser_window) \
#define ELECTRON_BROWSER_BINDINGS(V) \
V(electron_browser_app) \
V(electron_browser_auto_updater) \
V(electron_browser_content_tracing) \
V(electron_browser_crash_reporter) \
V(electron_browser_desktop_capturer) \
V(electron_browser_dialog) \
V(electron_browser_event_emitter) \
V(electron_browser_global_shortcut) \
V(electron_browser_image_view) \
V(electron_browser_in_app_purchase) \
V(electron_browser_menu) \
V(electron_browser_message_port) \
V(electron_browser_native_theme) \
V(electron_browser_notification) \
V(electron_browser_power_monitor) \
V(electron_browser_power_save_blocker) \
V(electron_browser_protocol) \
V(electron_browser_printing) \
V(electron_browser_push_notifications) \
V(electron_browser_safe_storage) \
V(electron_browser_service_worker_main) \
V(electron_browser_session) \
V(electron_browser_screen) \
V(electron_browser_system_preferences) \
V(electron_browser_base_window) \
V(electron_browser_tray) \
V(electron_browser_utility_process) \
V(electron_browser_view) \
V(electron_browser_web_contents) \
V(electron_browser_web_contents_view) \
V(electron_browser_web_frame_main) \
V(electron_browser_web_view_manager) \
V(electron_browser_window) \
V(electron_common_net)
#define ELECTRON_COMMON_BINDINGS(V) \

View File

@@ -288,6 +288,10 @@ inline constexpr base::cstring_view kEnableAuthNegotiatePort =
// If set, NTLM v2 is disabled for POSIX platforms.
inline constexpr base::cstring_view kDisableNTLMv2 = "disable-ntlm-v2";
// Indicates that preloads for service workers are registered.
inline constexpr base::cstring_view kServiceWorkerPreload =
"service-worker-preload";
} // namespace switches
} // namespace electron

View File

@@ -13,18 +13,23 @@
#include "base/containers/contains.h"
#include "base/feature_list.h"
#include "base/json/json_writer.h"
#include "base/trace_event/trace_event.h"
#include "content/public/renderer/render_frame.h"
#include "content/public/renderer/render_frame_observer.h"
#include "gin/converter.h"
#include "shell/common/gin_converters/blink_converter.h"
#include "shell/common/gin_converters/callback_converter.h"
#include "shell/common/gin_converters/value_converter.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/promise.h"
#include "shell/common/node_includes.h"
#include "shell/common/world_ids.h"
#include "shell/renderer/preload_realm_context.h"
#include "third_party/blink/public/web/web_blob.h"
#include "third_party/blink/public/web/web_element.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h" // nogncheck
namespace features {
BASE_FEATURE(kContextBridgeMutability,
@@ -133,8 +138,21 @@ v8::MaybeLocal<v8::Value> GetPrivate(v8::Local<v8::Context> context,
} // namespace
v8::MaybeLocal<v8::Value> PassValueToOtherContext(
// Forward declare methods
void ProxyFunctionWrapper(const v8::FunctionCallbackInfo<v8::Value>& info);
v8::MaybeLocal<v8::Object> CreateProxyForAPI(
const v8::Local<v8::Object>& api_object,
const v8::Local<v8::Context>& source_context,
const blink::ExecutionContext* source_execution_context,
const v8::Local<v8::Context>& destination_context,
context_bridge::ObjectCache* object_cache,
bool support_dynamic_properties,
int recursion_depth,
BridgeErrorTarget error_target);
v8::MaybeLocal<v8::Value> PassValueToOtherContextInner(
v8::Local<v8::Context> source_context,
const blink::ExecutionContext* source_execution_context,
v8::Local<v8::Context> destination_context,
v8::Local<v8::Value> value,
v8::Local<v8::Value> parent_value,
@@ -142,7 +160,7 @@ v8::MaybeLocal<v8::Value> PassValueToOtherContext(
bool support_dynamic_properties,
int recursion_depth,
BridgeErrorTarget error_target) {
TRACE_EVENT0("electron", "ContextBridge::PassValueToOtherContext");
TRACE_EVENT0("electron", "ContextBridge::PassValueToOtherContextInner");
if (recursion_depth >= kMaxRecursion) {
v8::Context::Scope error_scope(error_target == BridgeErrorTarget::kSource
? source_context
@@ -245,7 +263,6 @@ v8::MaybeLocal<v8::Value> PassValueToOtherContext(
if (global_source_context.IsEmpty() ||
global_destination_context.IsEmpty())
return;
context_bridge::ObjectCache object_cache;
v8::MaybeLocal<v8::Value> val;
{
v8::TryCatch try_catch(isolate);
@@ -253,7 +270,7 @@ v8::MaybeLocal<v8::Value> PassValueToOtherContext(
global_source_context.Get(isolate);
val = PassValueToOtherContext(
source_context, global_destination_context.Get(isolate), result,
source_context->Global(), &object_cache, false, 0,
source_context->Global(), false,
BridgeErrorTarget::kDestination);
if (try_catch.HasCaught()) {
if (try_catch.Message().IsEmpty()) {
@@ -293,7 +310,6 @@ v8::MaybeLocal<v8::Value> PassValueToOtherContext(
if (global_source_context.IsEmpty() ||
global_destination_context.IsEmpty())
return;
context_bridge::ObjectCache object_cache;
v8::MaybeLocal<v8::Value> val;
{
v8::TryCatch try_catch(isolate);
@@ -301,7 +317,7 @@ v8::MaybeLocal<v8::Value> PassValueToOtherContext(
global_source_context.Get(isolate);
val = PassValueToOtherContext(
source_context, global_destination_context.Get(isolate), result,
source_context->Global(), &object_cache, false, 0,
source_context->Global(), false,
BridgeErrorTarget::kDestination);
if (try_catch.HasCaught()) {
if (try_catch.Message().IsEmpty()) {
@@ -367,8 +383,8 @@ v8::MaybeLocal<v8::Value> PassValueToOtherContext(
v8::Local<v8::Array> cloned_arr =
v8::Array::New(destination_context->GetIsolate(), length);
for (size_t i = 0; i < length; i++) {
auto value_for_array = PassValueToOtherContext(
source_context, destination_context,
auto value_for_array = PassValueToOtherContextInner(
source_context, source_execution_context, destination_context,
arr->Get(source_context, i).ToLocalChecked(), value, object_cache,
support_dynamic_properties, recursion_depth + 1, error_target);
if (value_for_array.IsEmpty())
@@ -383,30 +399,34 @@ v8::MaybeLocal<v8::Value> PassValueToOtherContext(
return v8::MaybeLocal<v8::Value>(cloned_arr);
}
// Custom logic to "clone" Element references
blink::WebElement elem =
blink::WebElement::FromV8Value(destination_context->GetIsolate(), value);
if (!elem.IsNull()) {
v8::Context::Scope destination_context_scope(destination_context);
return v8::MaybeLocal<v8::Value>(
elem.ToV8Value(destination_context->GetIsolate()));
}
// Clone certain DOM APIs only within Window contexts.
if (source_execution_context->IsWindow()) {
// Custom logic to "clone" Element references
blink::WebElement elem = blink::WebElement::FromV8Value(
destination_context->GetIsolate(), value);
if (!elem.IsNull()) {
v8::Context::Scope destination_context_scope(destination_context);
return v8::MaybeLocal<v8::Value>(
elem.ToV8Value(destination_context->GetIsolate()));
}
// Custom logic to "clone" Blob references
blink::WebBlob blob =
blink::WebBlob::FromV8Value(destination_context->GetIsolate(), value);
if (!blob.IsNull()) {
v8::Context::Scope destination_context_scope(destination_context);
return v8::MaybeLocal<v8::Value>(
blob.ToV8Value(destination_context->GetIsolate()));
// Custom logic to "clone" Blob references
blink::WebBlob blob =
blink::WebBlob::FromV8Value(destination_context->GetIsolate(), value);
if (!blob.IsNull()) {
v8::Context::Scope destination_context_scope(destination_context);
return v8::MaybeLocal<v8::Value>(
blob.ToV8Value(destination_context->GetIsolate()));
}
}
// Proxy all objects
if (IsPlainObject(value)) {
auto object_value = value.As<v8::Object>();
auto passed_value = CreateProxyForAPI(
object_value, source_context, destination_context, object_cache,
support_dynamic_properties, recursion_depth + 1, error_target);
object_value, source_context, source_execution_context,
destination_context, object_cache, support_dynamic_properties,
recursion_depth + 1, error_target);
if (passed_value.IsEmpty())
return {};
return v8::MaybeLocal<v8::Value>(passed_value.ToLocalChecked());
@@ -434,6 +454,28 @@ v8::MaybeLocal<v8::Value> PassValueToOtherContext(
}
}
v8::MaybeLocal<v8::Value> PassValueToOtherContext(
v8::Local<v8::Context> source_context,
v8::Local<v8::Context> destination_context,
v8::Local<v8::Value> value,
v8::Local<v8::Value> parent_value,
bool support_dynamic_properties,
BridgeErrorTarget error_target,
context_bridge::ObjectCache* existing_object_cache) {
TRACE_EVENT0("electron", "ContextBridge::PassValueToOtherContext");
context_bridge::ObjectCache local_object_cache;
context_bridge::ObjectCache* object_cache =
existing_object_cache ? existing_object_cache : &local_object_cache;
const blink::ExecutionContext* source_execution_context =
blink::ExecutionContext::From(source_context);
DCHECK(source_execution_context);
return PassValueToOtherContextInner(
source_context, source_execution_context, destination_context, value,
parent_value, object_cache, support_dynamic_properties, 0, error_target);
}
void ProxyFunctionWrapper(const v8::FunctionCallbackInfo<v8::Value>& info) {
TRACE_EVENT0("electron", "ContextBridge::ProxyFunctionWrapper");
CHECK(info.Data()->IsObject());
@@ -464,6 +506,8 @@ void ProxyFunctionWrapper(const v8::FunctionCallbackInfo<v8::Value>& info) {
{
v8::Context::Scope func_owning_context_scope(func_owning_context);
// Cache duplicate arguments as the same proxied value.
context_bridge::ObjectCache object_cache;
std::vector<v8::Local<v8::Value>> original_args;
@@ -473,8 +517,8 @@ void ProxyFunctionWrapper(const v8::FunctionCallbackInfo<v8::Value>& info) {
for (auto value : original_args) {
auto arg = PassValueToOtherContext(
calling_context, func_owning_context, value,
calling_context->Global(), &object_cache, support_dynamic_properties,
0, BridgeErrorTarget::kSource);
calling_context->Global(), support_dynamic_properties,
BridgeErrorTarget::kSource, &object_cache);
if (arg.IsEmpty())
return;
proxied_args.push_back(arg.ToLocalChecked());
@@ -540,11 +584,10 @@ void ProxyFunctionWrapper(const v8::FunctionCallbackInfo<v8::Value>& info) {
v8::Local<v8::String> exception;
{
v8::TryCatch try_catch(args.isolate());
ret = PassValueToOtherContext(func_owning_context, calling_context,
maybe_return_value.ToLocalChecked(),
func_owning_context->Global(),
&object_cache, support_dynamic_properties,
0, BridgeErrorTarget::kDestination);
ret = PassValueToOtherContext(
func_owning_context, calling_context,
maybe_return_value.ToLocalChecked(), func_owning_context->Global(),
support_dynamic_properties, BridgeErrorTarget::kDestination);
if (try_catch.HasCaught()) {
did_error_converting_result = true;
if (!try_catch.Message().IsEmpty()) {
@@ -576,6 +619,7 @@ void ProxyFunctionWrapper(const v8::FunctionCallbackInfo<v8::Value>& info) {
v8::MaybeLocal<v8::Object> CreateProxyForAPI(
const v8::Local<v8::Object>& api_object,
const v8::Local<v8::Context>& source_context,
const blink::ExecutionContext* source_execution_context,
const v8::Local<v8::Context>& destination_context,
context_bridge::ObjectCache* object_cache,
bool support_dynamic_properties,
@@ -619,18 +663,20 @@ v8::MaybeLocal<v8::Object> CreateProxyForAPI(
v8::Local<v8::Value> getter_proxy;
v8::Local<v8::Value> setter_proxy;
if (!getter.IsEmpty()) {
if (!PassValueToOtherContext(
source_context, destination_context, getter,
api.GetHandle(), object_cache,
support_dynamic_properties, 1, error_target)
if (!PassValueToOtherContextInner(
source_context, source_execution_context,
destination_context, getter, api.GetHandle(),
object_cache, support_dynamic_properties, 1,
error_target)
.ToLocal(&getter_proxy))
continue;
}
if (!setter.IsEmpty()) {
if (!PassValueToOtherContext(
source_context, destination_context, setter,
api.GetHandle(), object_cache,
support_dynamic_properties, 1, error_target)
if (!PassValueToOtherContextInner(
source_context, source_execution_context,
destination_context, setter, api.GetHandle(),
object_cache, support_dynamic_properties, 1,
error_target)
.ToLocal(&setter_proxy))
continue;
}
@@ -646,10 +692,10 @@ v8::MaybeLocal<v8::Object> CreateProxyForAPI(
if (!api.Get(key, &value))
continue;
auto passed_value = PassValueToOtherContext(
source_context, destination_context, value, api.GetHandle(),
object_cache, support_dynamic_properties, recursion_depth + 1,
error_target);
auto passed_value = PassValueToOtherContextInner(
source_context, source_execution_context, destination_context, value,
api.GetHandle(), object_cache, support_dynamic_properties,
recursion_depth + 1, error_target);
if (passed_value.IsEmpty())
return {};
proxy.Set(key, passed_value.ToLocalChecked());
@@ -661,24 +707,14 @@ v8::MaybeLocal<v8::Object> CreateProxyForAPI(
namespace {
void ExposeAPIInWorld(v8::Isolate* isolate,
const int world_id,
const std::string& key,
v8::Local<v8::Value> api,
gin_helper::Arguments* args) {
TRACE_EVENT2("electron", "ContextBridge::ExposeAPIInWorld", "key", key,
"worldId", world_id);
auto* render_frame = GetRenderFrame(isolate->GetCurrentContext()->Global());
CHECK(render_frame);
auto* frame = render_frame->GetWebFrame();
CHECK(frame);
v8::Local<v8::Context> target_context =
world_id == WorldIDs::MAIN_WORLD_ID
? frame->MainWorldScriptContext()
: frame->GetScriptContextFromWorldId(isolate, world_id);
void ExposeAPI(v8::Isolate* isolate,
v8::Local<v8::Context> source_context,
v8::Local<v8::Context> target_context,
const std::string& key,
v8::Local<v8::Value> api,
gin_helper::Arguments* args) {
DCHECK(!target_context.IsEmpty());
v8::Context::Scope target_context_scope(target_context);
gin_helper::Dictionary global(target_context->GetIsolate(),
target_context->Global());
@@ -689,33 +725,78 @@ void ExposeAPIInWorld(v8::Isolate* isolate,
return;
}
v8::Local<v8::Context> electron_isolated_context =
frame->GetScriptContextFromWorldId(args->isolate(),
WorldIDs::ISOLATED_WORLD_ID);
v8::MaybeLocal<v8::Value> maybe_proxy = PassValueToOtherContext(
source_context, target_context, api, source_context->Global(), false,
BridgeErrorTarget::kSource);
if (maybe_proxy.IsEmpty())
return;
auto proxy = maybe_proxy.ToLocalChecked();
{
context_bridge::ObjectCache object_cache;
v8::Context::Scope target_context_scope(target_context);
v8::MaybeLocal<v8::Value> maybe_proxy = PassValueToOtherContext(
electron_isolated_context, target_context, api,
electron_isolated_context->Global(), &object_cache, false, 0,
BridgeErrorTarget::kSource);
if (maybe_proxy.IsEmpty())
return;
auto proxy = maybe_proxy.ToLocalChecked();
if (base::FeatureList::IsEnabled(features::kContextBridgeMutability)) {
global.Set(key, proxy);
return;
}
if (proxy->IsObject() && !proxy->IsTypedArray() &&
!DeepFreeze(proxy.As<v8::Object>(), target_context))
return;
global.SetReadOnlyNonConfigurable(key, proxy);
if (base::FeatureList::IsEnabled(features::kContextBridgeMutability)) {
global.Set(key, proxy);
return;
}
if (proxy->IsObject() && !proxy->IsTypedArray() &&
!DeepFreeze(proxy.As<v8::Object>(), target_context))
return;
global.SetReadOnlyNonConfigurable(key, proxy);
}
// Attempt to get the target context based on the current context.
//
// For render frames, this is either the main world (0) or an arbitrary
// world ID. For service workers, Electron only supports one isolated
// context and the main worker context. Anything else is invalid.
v8::MaybeLocal<v8::Context> GetTargetContext(v8::Isolate* isolate,
const int world_id) {
v8::Local<v8::Context> source_context = isolate->GetCurrentContext();
v8::MaybeLocal<v8::Context> maybe_target_context;
blink::ExecutionContext* execution_context =
blink::ExecutionContext::From(source_context);
if (execution_context->IsWindow()) {
auto* render_frame = GetRenderFrame(source_context->Global());
CHECK(render_frame);
auto* frame = render_frame->GetWebFrame();
CHECK(frame);
maybe_target_context =
world_id == WorldIDs::MAIN_WORLD_ID
? frame->MainWorldScriptContext()
: frame->GetScriptContextFromWorldId(isolate, world_id);
} else if (execution_context->IsShadowRealmGlobalScope()) {
if (world_id != WorldIDs::MAIN_WORLD_ID) {
isolate->ThrowException(v8::Exception::Error(gin::StringToV8(
isolate, "Isolated worlds are not supported in preload realms.")));
return maybe_target_context;
}
maybe_target_context =
electron::preload_realm::GetInitiatorContext(source_context);
} else {
NOTREACHED();
}
CHECK(!maybe_target_context.IsEmpty());
return maybe_target_context;
}
void ExposeAPIInWorld(v8::Isolate* isolate,
const int world_id,
const std::string& key,
v8::Local<v8::Value> api,
gin_helper::Arguments* args) {
TRACE_EVENT2("electron", "ContextBridge::ExposeAPIInWorld", "key", key,
"worldId", world_id);
v8::Local<v8::Context> source_context = isolate->GetCurrentContext();
CHECK(!source_context.IsEmpty());
v8::MaybeLocal<v8::Context> maybe_target_context =
GetTargetContext(isolate, world_id);
if (maybe_target_context.IsEmpty())
return;
v8::Local<v8::Context> target_context = maybe_target_context.ToLocalChecked();
ExposeAPI(isolate, source_context, target_context, key, api, args);
}
gin_helper::Dictionary TraceKeyPath(const gin_helper::Dictionary& start,
@@ -747,12 +828,10 @@ void OverrideGlobalValueFromIsolatedWorld(
{
v8::Context::Scope main_context_scope(main_context);
context_bridge::ObjectCache object_cache;
v8::Local<v8::Context> source_context = value->GetCreationContextChecked();
v8::MaybeLocal<v8::Value> maybe_proxy = PassValueToOtherContext(
source_context, main_context, value, source_context->Global(),
&object_cache, support_dynamic_properties, 1,
BridgeErrorTarget::kSource);
support_dynamic_properties, BridgeErrorTarget::kSource);
DCHECK(!maybe_proxy.IsEmpty());
auto proxy = maybe_proxy.ToLocalChecked();
@@ -789,8 +868,8 @@ bool OverrideGlobalPropertyFromIsolatedWorld(
v8::Local<v8::Context> source_context =
getter->GetCreationContextChecked();
v8::MaybeLocal<v8::Value> maybe_getter_proxy = PassValueToOtherContext(
source_context, main_context, getter, source_context->Global(),
&object_cache, false, 1, BridgeErrorTarget::kSource);
source_context, main_context, getter, source_context->Global(), false,
BridgeErrorTarget::kSource);
DCHECK(!maybe_getter_proxy.IsEmpty());
getter_proxy = maybe_getter_proxy.ToLocalChecked();
}
@@ -798,8 +877,8 @@ bool OverrideGlobalPropertyFromIsolatedWorld(
v8::Local<v8::Context> source_context =
getter->GetCreationContextChecked();
v8::MaybeLocal<v8::Value> maybe_setter_proxy = PassValueToOtherContext(
source_context, main_context, setter, source_context->Global(),
&object_cache, false, 1, BridgeErrorTarget::kSource);
source_context, main_context, setter, source_context->Global(), false,
BridgeErrorTarget::kSource);
DCHECK(!maybe_setter_proxy.IsEmpty());
setter_proxy = maybe_setter_proxy.ToLocalChecked();
}
@@ -812,13 +891,205 @@ bool OverrideGlobalPropertyFromIsolatedWorld(
}
}
bool IsCalledFromMainWorld(v8::Isolate* isolate) {
auto* render_frame = GetRenderFrame(isolate->GetCurrentContext()->Global());
CHECK(render_frame);
auto* frame = render_frame->GetWebFrame();
CHECK(frame);
v8::Local<v8::Context> main_context = frame->MainWorldScriptContext();
return isolate->GetCurrentContext() == main_context;
// Serialize script to be executed in the given world.
v8::Local<v8::Value> ExecuteInWorld(v8::Isolate* isolate,
const int world_id,
gin_helper::Arguments* args) {
// Get context of caller
v8::Local<v8::Context> source_context = isolate->GetCurrentContext();
// Get execution script argument
gin_helper::Dictionary exec_script;
if (args->Length() >= 1 && !args->GetNext(&exec_script)) {
gin_helper::ErrorThrower(args->isolate()).ThrowError("Invalid script");
return v8::Undefined(isolate);
}
// Get "func" from execution script
v8::Local<v8::Function> func;
if (!exec_script.Get("func", &func)) {
gin_helper::ErrorThrower(isolate).ThrowError(
"Function 'func' is required in script");
return v8::Undefined(isolate);
}
// Get optional "args" from execution script
v8::Local<v8::Array> args_array;
v8::Local<v8::Value> args_value;
if (exec_script.Get("args", &args_value)) {
if (!args_value->IsArray()) {
gin_helper::ErrorThrower(isolate).ThrowError("'args' must be an array");
return v8::Undefined(isolate);
}
args_array = args_value.As<v8::Array>();
}
// Serialize the function
std::string function_str;
{
v8::Local<v8::String> serialized_function;
if (!func->FunctionProtoToString(isolate->GetCurrentContext())
.ToLocal(&serialized_function)) {
gin_helper::ErrorThrower(isolate).ThrowError(
"Failed to serialize function");
return v8::Undefined(isolate);
}
// If ToLocal() succeeds, this should always be a string.
CHECK(gin::Converter<std::string>::FromV8(isolate, serialized_function,
&function_str));
}
// Get the target context
v8::MaybeLocal<v8::Context> maybe_target_context =
GetTargetContext(isolate, world_id);
v8::Local<v8::Context> target_context;
if (!maybe_target_context.ToLocal(&target_context)) {
isolate->ThrowException(v8::Exception::Error(gin::StringToV8(
isolate,
base::StringPrintf("Failed to get context for world %d", world_id))));
return v8::Undefined(isolate);
}
// Compile the script
v8::Local<v8::Script> compiled_script;
{
v8::Context::Scope target_scope(target_context);
std::string error_message;
v8::MaybeLocal<v8::Script> maybe_compiled_script;
{
v8::TryCatch try_catch(isolate);
std::string return_func_code =
base::StringPrintf("(%s)", function_str.c_str());
maybe_compiled_script = v8::Script::Compile(
target_context, gin::StringToV8(isolate, return_func_code));
if (try_catch.HasCaught()) {
// Must throw outside of TryCatch scope
v8::String::Utf8Value error(isolate, try_catch.Exception());
error_message =
*error ? *error : "Unknown error during script compilation";
}
}
if (!maybe_compiled_script.ToLocal(&compiled_script)) {
isolate->ThrowException(
v8::Exception::Error(gin::StringToV8(isolate, error_message)));
return v8::Undefined(isolate);
}
}
// Run the script
v8::Local<v8::Function> copied_func;
{
v8::Context::Scope target_scope(target_context);
std::string error_message;
v8::MaybeLocal<v8::Value> maybe_script_result;
{
v8::TryCatch try_catch(isolate);
maybe_script_result = compiled_script->Run(target_context);
if (try_catch.HasCaught()) {
// Must throw outside of TryCatch scope
v8::String::Utf8Value error(isolate, try_catch.Exception());
error_message =
*error ? *error : "Unknown error during script execution";
}
}
v8::Local<v8::Value> script_result;
if (!maybe_script_result.ToLocal(&script_result)) {
isolate->ThrowException(
v8::Exception::Error(gin::StringToV8(isolate, error_message)));
return v8::Undefined(isolate);
}
if (!script_result->IsFunction()) {
isolate->ThrowException(v8::Exception::Error(
gin::StringToV8(isolate,
"Expected script to result in a function but a "
"non-function type was found")));
return v8::Undefined(isolate);
}
// Get copied function from the script result
copied_func = script_result.As<v8::Function>();
}
// Proxy args to be passed into copied function
std::vector<v8::Local<v8::Value>> proxied_args;
{
v8::Context::Scope target_scope(target_context);
bool support_dynamic_properties = false;
uint32_t args_length = args_array.IsEmpty() ? 0 : args_array->Length();
// Cache duplicate arguments as the same proxied value.
context_bridge::ObjectCache object_cache;
for (uint32_t i = 0; i < args_length; ++i) {
v8::Local<v8::Value> arg;
if (!args_array->Get(source_context, i).ToLocal(&arg)) {
gin_helper::ErrorThrower(isolate).ThrowError(
base::StringPrintf("Failed to get argument at index %d", i));
return v8::Undefined(isolate);
}
auto proxied_arg = PassValueToOtherContext(
source_context, target_context, arg, source_context->Global(),
support_dynamic_properties, BridgeErrorTarget::kSource,
&object_cache);
if (proxied_arg.IsEmpty()) {
gin_helper::ErrorThrower(isolate).ThrowError(
base::StringPrintf("Failed to proxy argument at index %d", i));
return v8::Undefined(isolate);
}
proxied_args.push_back(proxied_arg.ToLocalChecked());
}
}
// Call the function and get the result
v8::Local<v8::Value> result;
{
v8::Context::Scope target_scope(target_context);
std::string error_message;
v8::MaybeLocal<v8::Value> maybe_result;
{
v8::TryCatch try_catch(isolate);
maybe_result =
copied_func->Call(isolate, target_context, v8::Null(isolate),
proxied_args.size(), proxied_args.data());
if (try_catch.HasCaught()) {
v8::String::Utf8Value error(isolate, try_catch.Exception());
error_message =
*error ? *error : "Unknown error during function execution";
}
}
if (!maybe_result.ToLocal(&result)) {
// Must throw outside of TryCatch scope
isolate->ThrowException(
v8::Exception::Error(gin::StringToV8(isolate, error_message)));
return v8::Undefined(isolate);
}
}
// Clone the result into the source/caller context
v8::Local<v8::Value> cloned_result;
{
v8::Context::Scope source_scope(source_context);
std::string error_message;
v8::MaybeLocal<v8::Value> maybe_cloned_result;
{
v8::TryCatch try_catch(isolate);
// Pass value from target context back to source context
maybe_cloned_result = PassValueToOtherContext(
target_context, source_context, result, target_context->Global(),
false, BridgeErrorTarget::kSource);
if (try_catch.HasCaught()) {
v8::String::Utf8Value utf8(isolate, try_catch.Exception());
error_message = *utf8 ? *utf8 : "Unknown error cloning result";
}
}
if (!maybe_cloned_result.ToLocal(&cloned_result)) {
// Must throw outside of TryCatch scope
isolate->ThrowException(
v8::Exception::Error(gin::StringToV8(isolate, error_message)));
return v8::Undefined(isolate);
}
}
return cloned_result;
}
} // namespace
@@ -835,13 +1106,12 @@ void Initialize(v8::Local<v8::Object> exports,
void* priv) {
v8::Isolate* isolate = context->GetIsolate();
gin_helper::Dictionary dict(isolate, exports);
dict.SetMethod("executeInWorld", &electron::api::ExecuteInWorld);
dict.SetMethod("exposeAPIInWorld", &electron::api::ExposeAPIInWorld);
dict.SetMethod("_overrideGlobalValueFromIsolatedWorld",
&electron::api::OverrideGlobalValueFromIsolatedWorld);
dict.SetMethod("_overrideGlobalPropertyFromIsolatedWorld",
&electron::api::OverrideGlobalPropertyFromIsolatedWorld);
dict.SetMethod("_isCalledFromMainWorld",
&electron::api::IsCalledFromMainWorld);
#if DCHECK_IS_ON()
dict.Set("_isDebug", true);
#endif

View File

@@ -14,8 +14,6 @@ class Arguments;
namespace electron::api {
void ProxyFunctionWrapper(const v8::FunctionCallbackInfo<v8::Value>& info);
// Where the context bridge should create the exception it is about to throw
enum class BridgeErrorTarget {
// The source / calling context. This is default and correct 99% of the time,
@@ -44,19 +42,9 @@ v8::MaybeLocal<v8::Value> PassValueToOtherContext(
* the bridge set this to the "context" of the value.
*/
v8::Local<v8::Value> parent_value,
context_bridge::ObjectCache* object_cache,
bool support_dynamic_properties,
int recursion_depth,
BridgeErrorTarget error_target);
v8::MaybeLocal<v8::Object> CreateProxyForAPI(
const v8::Local<v8::Object>& api_object,
const v8::Local<v8::Context>& source_context,
const v8::Local<v8::Context>& destination_context,
context_bridge::ObjectCache* object_cache,
bool support_dynamic_properties,
int recursion_depth,
BridgeErrorTarget error_target);
BridgeErrorTarget error_target,
context_bridge::ObjectCache* existing_object_cache = nullptr);
} // namespace electron::api

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"
@@ -14,15 +15,20 @@
#include "shell/common/api/api.mojom.h"
#include "shell/common/gin_converters/blink_converter.h"
#include "shell/common/gin_converters/value_converter.h"
#include "shell/common/gin_helper/dictionary.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_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,50 +46,23 @@ RenderFrame* GetCurrentRenderFrame() {
return RenderFrame::FromWebFrame(frame);
}
class IPCRenderer final : public gin::Wrappable<IPCRenderer>,
private content::RenderFrameObserver {
// 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;
}
template <typename T>
class IPCBase : public gin::Wrappable<T> {
public:
static gin::WrapperInfo kWrapperInfo;
static gin::Handle<IPCRenderer> Create(v8::Isolate* isolate) {
return gin::CreateHandle(isolate, new IPCRenderer(isolate));
static gin::Handle<T> Create(v8::Isolate* isolate) {
return gin::CreateHandle(isolate, new T(isolate));
}
explicit IPCRenderer(v8::Isolate* isolate)
: content::RenderFrameObserver(GetCurrentRenderFrame()) {
RenderFrame* render_frame = GetCurrentRenderFrame();
DCHECK(render_frame);
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(); }
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();
}
// gin::Wrappable:
gin::ObjectTemplateBuilder GetObjectTemplateBuilder(
v8::Isolate* isolate) override {
return gin::Wrappable<IPCRenderer>::GetObjectTemplateBuilder(isolate)
.SetMethod("send", &IPCRenderer::SendMessage)
.SetMethod("sendSync", &IPCRenderer::SendSync)
.SetMethod("sendToHost", &IPCRenderer::SendToHost)
.SetMethod("invoke", &IPCRenderer::Invoke)
.SetMethod("postMessage", &IPCRenderer::PostMessage);
}
const char* GetTypeName() override { return "IPCRenderer"; }
private:
void SendMessage(v8::Isolate* isolate,
gin_helper::ErrorThrower thrower,
bool internal,
@@ -202,18 +181,95 @@ class IPCRenderer final : public gin::Wrappable<IPCRenderer>,
return electron::DeserializeV8Value(isolate, result);
}
v8::Global<v8::Context> weak_context_;
// gin::Wrappable:
gin::ObjectTemplateBuilder GetObjectTemplateBuilder(
v8::Isolate* isolate) override {
return gin::Wrappable<T>::GetObjectTemplateBuilder(isolate)
.SetMethod("send", &T::SendMessage)
.SetMethod("sendSync", &T::SendSync)
.SetMethod("sendToHost", &T::SendToHost)
.SetMethod("invoke", &T::Invoke)
.SetMethod("postMessage", &T::PostMessage);
}
protected:
mojo::AssociatedRemote<electron::mojom::ElectronApiIPC> electron_ipc_remote_;
};
gin::WrapperInfo IPCRenderer::kWrapperInfo = {gin::kEmbedderNativeGin};
class IPCRenderFrame : public IPCBase<IPCRenderFrame>,
private content::RenderFrameObserver {
public:
explicit IPCRenderFrame(v8::Isolate* isolate)
: content::RenderFrameObserver(GetCurrentRenderFrame()) {
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 {
NOTREACHED();
}
weak_context_ =
v8::Global<v8::Context>(isolate, isolate->GetCurrentContext());
weak_context_.SetWeak();
}
void OnDestruct() override { electron_ipc_remote_.reset(); }
void WillReleaseScriptContext(v8::Local<v8::Context> context,
int32_t world_id) override {
if (weak_context_.IsEmpty() ||
weak_context_.Get(context->GetIsolate()) == context) {
OnDestruct();
}
}
const char* GetTypeName() override { return "IPCRenderFrame"; }
private:
v8::Global<v8::Context> weak_context_;
};
template <>
gin::WrapperInfo IPCBase<IPCRenderFrame>::kWrapperInfo = {
gin::kEmbedderNativeGin};
class IPCServiceWorker : public IPCBase<IPCServiceWorker>,
public content::WorkerThread::Observer {
public:
explicit IPCServiceWorker(v8::Isolate* isolate) {
DCHECK(IsWorkerThread());
content::WorkerThread::AddObserver(this);
electron::ServiceWorkerData* service_worker_data =
electron::preload_realm::GetServiceWorkerData(
isolate->GetCurrentContext());
DCHECK(service_worker_data);
service_worker_data->proxy()->GetRemoteAssociatedInterface(
electron_ipc_remote_.BindNewEndpointAndPassReceiver());
}
void WillStopCurrentWorkerThread() override { electron_ipc_remote_.reset(); }
const char* GetTypeName() override { return "IPCServiceWorker"; }
};
template <>
gin::WrapperInfo IPCBase<IPCServiceWorker>::kWrapperInfo = {
gin::kEmbedderNativeGin};
void Initialize(v8::Local<v8::Object> exports,
v8::Local<v8::Value> unused,
v8::Local<v8::Context> context,
void* priv) {
gin::Dictionary dict(context->GetIsolate(), exports);
dict.Set("ipc", IPCRenderer::Create(context->GetIsolate()));
gin_helper::Dictionary dict(context->GetIsolate(), exports);
dict.SetMethod("createForRenderFrame", &IPCRenderFrame::Create);
dict.SetMethod("createForServiceWorker", &IPCServiceWorker::Create);
}
} // namespace

View File

@@ -150,13 +150,11 @@ class ScriptExecutionCallback {
"An unknown exception occurred while getting the result of the script";
{
v8::TryCatch try_catch(isolate);
context_bridge::ObjectCache object_cache;
v8::Local<v8::Context> source_context =
result->GetCreationContextChecked();
maybe_result =
PassValueToOtherContext(source_context, promise_.GetContext(), result,
source_context->Global(), &object_cache,
false, 0, BridgeErrorTarget::kSource);
maybe_result = PassValueToOtherContext(
source_context, promise_.GetContext(), result,
source_context->Global(), false, BridgeErrorTarget::kSource);
if (maybe_result.IsEmpty() || try_catch.HasCaught()) {
success = false;
}

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

@@ -11,18 +11,19 @@
#include "base/base_paths.h"
#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/process/process_handle.h"
#include "base/process/process_metrics.h"
#include "content/public/renderer/render_frame.h"
#include "shell/common/api/electron_bindings.h"
#include "shell/common/application_info.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/microtasks_scope.h"
#include "shell/common/node_bindings.h"
#include "shell/common/node_includes.h"
#include "shell/common/node_util.h"
#include "shell/common/options_switches.h"
#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,67 +34,10 @@ 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";
constexpr std::string_view kBindingCacheKey = "native-binding-cache";
v8::Local<v8::Object> GetBindingCache(v8::Isolate* isolate) {
auto context = isolate->GetCurrentContext();
gin_helper::Dictionary global(isolate, context->Global());
v8::Local<v8::Value> cache;
if (!global.GetHidden(kBindingCacheKey, &cache)) {
cache = v8::Object::New(isolate);
global.SetHidden(kBindingCacheKey, cache);
}
return cache->ToObject(context).ToLocalChecked();
}
// adapted from node.cc
v8::Local<v8::Value> GetBinding(v8::Isolate* isolate,
v8::Local<v8::String> key,
gin_helper::Arguments* margs) {
v8::Local<v8::Object> exports;
std::string binding_key = gin::V8ToString(isolate, key);
gin_helper::Dictionary cache(isolate, GetBindingCache(isolate));
if (cache.Get(binding_key, &exports)) {
return exports;
}
auto* mod = node::binding::get_linked_module(binding_key.c_str());
if (!mod) {
char errmsg[1024];
snprintf(errmsg, sizeof(errmsg), "No such binding: %s",
binding_key.c_str());
margs->ThrowError(errmsg);
return exports;
}
exports = v8::Object::New(isolate);
DCHECK_EQ(mod->nm_register_func, nullptr);
DCHECK_NE(mod->nm_context_register_func, nullptr);
mod->nm_context_register_func(exports, v8::Null(isolate),
isolate->GetCurrentContext(), mod->nm_priv);
cache.Set(binding_key, exports);
return exports;
}
v8::Local<v8::Value> CreatePreloadScript(v8::Isolate* isolate,
v8::Local<v8::String> source) {
auto context = isolate->GetCurrentContext();
auto maybe_script = v8::Script::Compile(context, source);
v8::Local<v8::Script> script;
if (!maybe_script.ToLocal(&script))
return {};
return script->Run(context).ToLocalChecked();
}
double Uptime() {
return (base::Time::Now() - base::Process::Current().CreationTime())
.InSecondsF();
}
void InvokeEmitProcessEvent(v8::Local<v8::Context> context,
const std::string& event_name) {
@@ -132,8 +76,8 @@ void ElectronSandboxedRendererClient::InitializeBindings(
content::RenderFrame* render_frame) {
auto* isolate = context->GetIsolate();
gin_helper::Dictionary b(isolate, binding);
b.SetMethod("get", GetBinding);
b.SetMethod("createPreloadScript", CreatePreloadScript);
b.SetMethod("get", preload_utils::GetBinding);
b.SetMethod("createPreloadScript", preload_utils::CreatePreloadScript);
auto process = gin_helper::Dictionary::CreateEmpty(isolate);
b.Set("process", process);
@@ -141,7 +85,7 @@ void ElectronSandboxedRendererClient::InitializeBindings(
ElectronBindings::BindProcess(isolate, &process, metrics_.get());
BindProcess(isolate, &process, render_frame);
process.SetMethod("uptime", Uptime);
process.SetMethod("uptime", preload_utils::Uptime);
process.Set("argv", base::CommandLine::ForCurrentProcess()->argv());
process.SetReadOnly("pid", base::GetCurrentProcId());
process.SetReadOnly("sandboxed", true);
@@ -231,4 +175,44 @@ void ElectronSandboxedRendererClient::EmitProcessEvent(
InvokeEmitProcessEvent(context, event_name);
}
void ElectronSandboxedRendererClient::WillEvaluateServiceWorkerOnWorkerThread(
blink::WebServiceWorkerContextProxy* context_proxy,
v8::Local<v8::Context> v8_context,
int64_t service_worker_version_id,
const GURL& service_worker_scope,
const GURL& script_url,
const blink::ServiceWorkerToken& service_worker_token) {
RendererClientBase::WillEvaluateServiceWorkerOnWorkerThread(
context_proxy, v8_context, service_worker_version_id,
service_worker_scope, script_url, service_worker_token);
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);
}
}
void ElectronSandboxedRendererClient::
WillDestroyServiceWorkerContextOnWorkerThread(
v8::Local<v8::Context> context,
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);
}
} // namespace electron

View File

@@ -42,6 +42,18 @@ class ElectronSandboxedRendererClient : public RendererClientBase {
void RenderFrameCreated(content::RenderFrame*) override;
void RunScriptsAtDocumentStart(content::RenderFrame* render_frame) override;
void RunScriptsAtDocumentEnd(content::RenderFrame* render_frame) override;
void WillEvaluateServiceWorkerOnWorkerThread(
blink::WebServiceWorkerContextProxy* context_proxy,
v8::Local<v8::Context> v8_context,
int64_t service_worker_version_id,
const GURL& service_worker_scope,
const GURL& script_url,
const blink::ServiceWorkerToken& service_worker_token) override;
void WillDestroyServiceWorkerContextOnWorkerThread(
v8::Local<v8::Context> context,
int64_t service_worker_version_id,
const GURL& service_worker_scope,
const GURL& script_url) override;
private:
void EmitProcessEvent(content::RenderFrame* render_frame,

View File

@@ -0,0 +1,295 @@
// 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/renderer/preload_realm_context.h"
#include "base/command_line.h"
#include "base/process/process.h"
#include "base/process/process_metrics.h"
#include "shell/common/api/electron_bindings.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/node_includes.h"
#include "shell/common/node_util.h"
#include "shell/renderer/preload_utils.h"
#include "shell/renderer/service_worker_data.h"
#include "third_party/blink/renderer/bindings/core/v8/script_controller.h" // nogncheck
#include "third_party/blink/renderer/core/execution_context/execution_context.h" // nogncheck
#include "third_party/blink/renderer/core/inspector/worker_thread_debugger.h" // nogncheck
#include "third_party/blink/renderer/core/shadow_realm/shadow_realm_global_scope.h" // nogncheck
#include "third_party/blink/renderer/core/workers/worker_or_worklet_global_scope.h" // nogncheck
#include "third_party/blink/renderer/platform/bindings/script_state.h" // nogncheck
#include "third_party/blink/renderer/platform/bindings/v8_dom_wrapper.h" // nogncheck
#include "third_party/blink/renderer/platform/bindings/v8_per_context_data.h" // nogncheck
#include "third_party/blink/renderer/platform/context_lifecycle_observer.h" // nogncheck
#include "v8/include/v8-context.h"
namespace electron::preload_realm {
namespace {
static constexpr int kElectronContextEmbedderDataIndex =
static_cast<int>(gin::kPerContextDataStartIndex) +
static_cast<int>(gin::kEmbedderElectron);
// This is a helper class to make the initiator ExecutionContext the owner
// of a ShadowRealmGlobalScope and its ScriptState. When the initiator
// ExecutionContext is destroyed, the ShadowRealmGlobalScope is destroyed,
// too.
class PreloadRealmLifetimeController
: public blink::GarbageCollected<PreloadRealmLifetimeController>,
public blink::ContextLifecycleObserver {
public:
explicit PreloadRealmLifetimeController(
blink::ExecutionContext* initiator_execution_context,
blink::ScriptState* initiator_script_state,
blink::ShadowRealmGlobalScope* shadow_realm_global_scope,
blink::ScriptState* shadow_realm_script_state,
electron::ServiceWorkerData* service_worker_data)
: initiator_script_state_(initiator_script_state),
is_initiator_worker_or_worklet_(
initiator_execution_context->IsWorkerOrWorkletGlobalScope()),
shadow_realm_global_scope_(shadow_realm_global_scope),
shadow_realm_script_state_(shadow_realm_script_state),
service_worker_data_(service_worker_data) {
// Align lifetime of this controller to that of the initiator's context.
self_ = this;
SetContextLifecycleNotifier(initiator_execution_context);
RegisterDebugger(initiator_execution_context);
initiator_context()->SetAlignedPointerInEmbedderData(
kElectronContextEmbedderDataIndex, static_cast<void*>(this));
realm_context()->SetAlignedPointerInEmbedderData(
kElectronContextEmbedderDataIndex, static_cast<void*>(this));
metrics_ = base::ProcessMetrics::CreateCurrentProcessMetrics();
RunInitScript();
}
static PreloadRealmLifetimeController* From(v8::Local<v8::Context> context) {
if (context->GetNumberOfEmbedderDataFields() <=
kElectronContextEmbedderDataIndex) {
return nullptr;
}
auto* controller = static_cast<PreloadRealmLifetimeController*>(
context->GetAlignedPointerFromEmbedderData(
kElectronContextEmbedderDataIndex));
CHECK(controller);
return controller;
}
void Trace(blink::Visitor* visitor) const override {
visitor->Trace(initiator_script_state_);
visitor->Trace(shadow_realm_global_scope_);
visitor->Trace(shadow_realm_script_state_);
ContextLifecycleObserver::Trace(visitor);
}
v8::MaybeLocal<v8::Context> GetContext() {
return shadow_realm_script_state_->ContextIsValid()
? shadow_realm_script_state_->GetContext()
: v8::MaybeLocal<v8::Context>();
}
v8::MaybeLocal<v8::Context> GetInitiatorContext() {
return initiator_script_state_->ContextIsValid()
? initiator_script_state_->GetContext()
: v8::MaybeLocal<v8::Context>();
}
electron::ServiceWorkerData* service_worker_data() {
return service_worker_data_;
}
protected:
void ContextDestroyed() override {
v8::HandleScope handle_scope(realm_isolate());
realm_context()->SetAlignedPointerInEmbedderData(
kElectronContextEmbedderDataIndex, nullptr);
// See ShadowRealmGlobalScope::ContextDestroyed
shadow_realm_script_state_->DisposePerContextData();
if (is_initiator_worker_or_worklet_) {
shadow_realm_script_state_->DissociateContext();
}
shadow_realm_script_state_.Clear();
shadow_realm_global_scope_->NotifyContextDestroyed();
shadow_realm_global_scope_.Clear();
self_.Clear();
}
private:
v8::Isolate* realm_isolate() {
return shadow_realm_script_state_->GetIsolate();
}
v8::Local<v8::Context> realm_context() {
return shadow_realm_script_state_->GetContext();
}
v8::Local<v8::Context> initiator_context() {
return initiator_script_state_->GetContext();
}
void RegisterDebugger(blink::ExecutionContext* initiator_execution_context) {
v8::Isolate* isolate = realm_isolate();
v8::Local<v8::Context> context = realm_context();
blink::WorkerThreadDebugger* debugger =
blink::WorkerThreadDebugger::From(isolate);
;
const auto* worker_context =
To<blink::WorkerOrWorkletGlobalScope>(initiator_execution_context);
// Override path to make preload realm easier to find in debugger.
blink::KURL url_for_debugger(worker_context->Url());
url_for_debugger.SetPath("electron-preload-realm");
debugger->ContextCreated(worker_context->GetThread(), url_for_debugger,
context);
}
void RunInitScript() {
v8::Isolate* isolate = realm_isolate();
v8::Local<v8::Context> context = realm_context();
v8::Context::Scope context_scope(context);
v8::MicrotasksScope microtasks_scope(
isolate, context->GetMicrotaskQueue(),
v8::MicrotasksScope::kDoNotRunMicrotasks);
v8::Local<v8::Object> binding = v8::Object::New(isolate);
gin_helper::Dictionary b(isolate, binding);
b.SetMethod("get", preload_utils::GetBinding);
b.SetMethod("createPreloadScript", preload_utils::CreatePreloadScript);
gin_helper::Dictionary process = gin::Dictionary::CreateEmpty(isolate);
b.Set("process", process);
ElectronBindings::BindProcess(isolate, &process, metrics_.get());
process.SetMethod("uptime", preload_utils::Uptime);
process.Set("argv", base::CommandLine::ForCurrentProcess()->argv());
process.SetReadOnly("pid", base::GetCurrentProcId());
process.SetReadOnly("sandboxed", true);
process.SetReadOnly("type", "service-worker");
process.SetReadOnly("contextIsolated", true);
std::vector<v8::Local<v8::String>> preload_realm_bundle_params = {
node::FIXED_ONE_BYTE_STRING(isolate, "binding")};
std::vector<v8::Local<v8::Value>> preload_realm_bundle_args = {binding};
util::CompileAndCall(context, "electron/js2c/preload_realm_bundle",
&preload_realm_bundle_params,
&preload_realm_bundle_args);
}
const blink::WeakMember<blink::ScriptState> initiator_script_state_;
bool is_initiator_worker_or_worklet_;
blink::Member<blink::ShadowRealmGlobalScope> shadow_realm_global_scope_;
blink::Member<blink::ScriptState> shadow_realm_script_state_;
std::unique_ptr<base::ProcessMetrics> metrics_;
raw_ptr<ServiceWorkerData> service_worker_data_;
blink::Persistent<PreloadRealmLifetimeController> self_;
};
} // namespace
v8::MaybeLocal<v8::Context> GetInitiatorContext(
v8::Local<v8::Context> context) {
DCHECK(!context.IsEmpty());
blink::ExecutionContext* execution_context =
blink::ExecutionContext::From(context);
if (!execution_context->IsShadowRealmGlobalScope())
return v8::MaybeLocal<v8::Context>();
auto* controller = PreloadRealmLifetimeController::From(context);
if (controller)
return controller->GetInitiatorContext();
return v8::MaybeLocal<v8::Context>();
}
v8::MaybeLocal<v8::Context> GetPreloadRealmContext(
v8::Local<v8::Context> context) {
DCHECK(!context.IsEmpty());
blink::ExecutionContext* execution_context =
blink::ExecutionContext::From(context);
if (!execution_context->IsServiceWorkerGlobalScope())
return v8::MaybeLocal<v8::Context>();
auto* controller = PreloadRealmLifetimeController::From(context);
if (controller)
return controller->GetContext();
return v8::MaybeLocal<v8::Context>();
}
electron::ServiceWorkerData* GetServiceWorkerData(
v8::Local<v8::Context> context) {
auto* controller = PreloadRealmLifetimeController::From(context);
return controller ? controller->service_worker_data() : nullptr;
}
void OnCreatePreloadableV8Context(
v8::Local<v8::Context> initiator_context,
electron::ServiceWorkerData* service_worker_data) {
v8::Isolate* isolate = initiator_context->GetIsolate();
blink::ScriptState* initiator_script_state =
blink::ScriptState::MaybeFrom(isolate, initiator_context);
DCHECK(initiator_script_state);
blink::ExecutionContext* initiator_execution_context =
blink::ExecutionContext::From(initiator_context);
DCHECK(initiator_execution_context);
blink::DOMWrapperWorld* world = blink::DOMWrapperWorld::Create(
isolate, blink::DOMWrapperWorld::WorldType::kShadowRealm);
CHECK(world); // Not yet run out of the world id.
// Create a new ShadowRealmGlobalScope.
blink::ShadowRealmGlobalScope* shadow_realm_global_scope =
blink::MakeGarbageCollected<blink::ShadowRealmGlobalScope>(
initiator_execution_context);
const blink::WrapperTypeInfo* wrapper_type_info =
shadow_realm_global_scope->GetWrapperTypeInfo();
// Create a new v8::Context.
// Initialize V8 extensions before creating the context.
v8::ExtensionConfiguration extension_configuration =
blink::ScriptController::ExtensionsFor(shadow_realm_global_scope);
v8::Local<v8::ObjectTemplate> global_template =
wrapper_type_info->GetV8ClassTemplate(isolate, *world)
.As<v8::FunctionTemplate>()
->InstanceTemplate();
v8::Local<v8::Object> global_proxy; // Will request a new global proxy.
v8::Local<v8::Context> context =
v8::Context::New(isolate, &extension_configuration, global_template,
global_proxy, v8::DeserializeInternalFieldsCallback(),
initiator_execution_context->GetMicrotaskQueue());
context->UseDefaultSecurityToken();
// Associate the Blink object with the v8::Context.
blink::ScriptState* script_state =
blink::ScriptState::Create(context, world, shadow_realm_global_scope);
// Associate the Blink object with the v8::Objects.
global_proxy = context->Global();
blink::V8DOMWrapper::SetNativeInfo(isolate, global_proxy,
shadow_realm_global_scope);
v8::Local<v8::Object> global_object =
global_proxy->GetPrototype().As<v8::Object>();
blink::V8DOMWrapper::SetNativeInfo(isolate, global_object,
shadow_realm_global_scope);
// Install context-dependent properties.
std::ignore =
script_state->PerContextData()->ConstructorForType(wrapper_type_info);
// Make the initiator execution context the owner of the
// ShadowRealmGlobalScope and the ScriptState.
blink::MakeGarbageCollected<PreloadRealmLifetimeController>(
initiator_execution_context, initiator_script_state,
shadow_realm_global_scope, script_state, service_worker_data);
}
} // namespace electron::preload_realm

View File

@@ -0,0 +1,34 @@
// 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_PRELOAD_REALM_CONTEXT_H_
#define ELECTRON_SHELL_RENDERER_PRELOAD_REALM_CONTEXT_H_
#include "v8/include/v8-forward.h"
namespace electron {
class ServiceWorkerData;
}
namespace electron::preload_realm {
// Get initiator context given the preload context.
v8::MaybeLocal<v8::Context> GetInitiatorContext(v8::Local<v8::Context> context);
// Get the preload context given the initiator context.
v8::MaybeLocal<v8::Context> GetPreloadRealmContext(
v8::Local<v8::Context> context);
// Get service worker data given the preload realm context.
electron::ServiceWorkerData* GetServiceWorkerData(
v8::Local<v8::Context> context);
// Create
void OnCreatePreloadableV8Context(
v8::Local<v8::Context> initiator_context,
electron::ServiceWorkerData* service_worker_data);
} // namespace electron::preload_realm
#endif // ELECTRON_SHELL_RENDERER_PRELOAD_REALM_CONTEXT_H_

View File

@@ -0,0 +1,80 @@
// Copyright (c) 2016 GitHub, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/renderer/preload_utils.h"
#include "base/process/process.h"
#include "shell/common/gin_helper/arguments.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/node_includes.h"
#include "v8/include/v8-context.h"
namespace electron::preload_utils {
namespace {
constexpr std::string_view kBindingCacheKey = "native-binding-cache";
v8::Local<v8::Object> GetBindingCache(v8::Isolate* isolate) {
auto context = isolate->GetCurrentContext();
gin_helper::Dictionary global(isolate, context->Global());
v8::Local<v8::Value> cache;
if (!global.GetHidden(kBindingCacheKey, &cache)) {
cache = v8::Object::New(isolate);
global.SetHidden(kBindingCacheKey, cache);
}
return cache->ToObject(context).ToLocalChecked();
}
} // namespace
// adapted from node.cc
v8::Local<v8::Value> GetBinding(v8::Isolate* isolate,
v8::Local<v8::String> key,
gin_helper::Arguments* margs) {
v8::Local<v8::Object> exports;
std::string binding_key = gin::V8ToString(isolate, key);
gin_helper::Dictionary cache(isolate, GetBindingCache(isolate));
if (cache.Get(binding_key, &exports)) {
return exports;
}
auto* mod = node::binding::get_linked_module(binding_key.c_str());
if (!mod) {
char errmsg[1024];
snprintf(errmsg, sizeof(errmsg), "No such binding: %s",
binding_key.c_str());
margs->ThrowError(errmsg);
return exports;
}
exports = v8::Object::New(isolate);
DCHECK_EQ(mod->nm_register_func, nullptr);
DCHECK_NE(mod->nm_context_register_func, nullptr);
mod->nm_context_register_func(exports, v8::Null(isolate),
isolate->GetCurrentContext(), mod->nm_priv);
cache.Set(binding_key, exports);
return exports;
}
v8::Local<v8::Value> CreatePreloadScript(v8::Isolate* isolate,
v8::Local<v8::String> source) {
auto context = isolate->GetCurrentContext();
auto maybe_script = v8::Script::Compile(context, source);
v8::Local<v8::Script> script;
if (!maybe_script.ToLocal(&script))
return {};
return script->Run(context).ToLocalChecked();
}
double Uptime() {
return (base::Time::Now() - base::Process::Current().CreationTime())
.InSecondsF();
}
} // namespace electron::preload_utils

View File

@@ -0,0 +1,27 @@
// Copyright (c) 2016 GitHub, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_RENDERER_PRELOAD_UTILS_H_
#define ELECTRON_SHELL_RENDERER_PRELOAD_UTILS_H_
#include "v8/include/v8-forward.h"
namespace gin_helper {
class Arguments;
}
namespace electron::preload_utils {
v8::Local<v8::Value> GetBinding(v8::Isolate* isolate,
v8::Local<v8::String> key,
gin_helper::Arguments* margs);
v8::Local<v8::Value> CreatePreloadScript(v8::Isolate* isolate,
v8::Local<v8::String> source);
double Uptime();
} // namespace electron::preload_utils
#endif // ELECTRON_SHELL_RENDERER_PRELOAD_UTILS_H_

View File

@@ -611,10 +611,9 @@ void RendererClientBase::SetupMainWorldOverrides(
v8::Local<v8::Value> guest_view_internal;
if (global.GetHidden("guestViewInternal", &guest_view_internal)) {
api::context_bridge::ObjectCache object_cache;
auto result = api::PassValueToOtherContext(
source_context, context, guest_view_internal, source_context->Global(),
&object_cache, false, 0, api::BridgeErrorTarget::kSource);
false, api::BridgeErrorTarget::kSource);
if (!result.IsEmpty()) {
isolated_api.Set("guestViewInternal", result.ToLocalChecked());
}

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

@@ -108,7 +108,7 @@ describe('contextBridge', () => {
};
const callWithBindings = (fn: Function, worldId: number = 0) =>
worldId === 0 ? w.webContents.executeJavaScript(`(${fn.toString()})(window)`) : w.webContents.executeJavaScriptInIsolatedWorld(worldId, [{ code: `(${fn.toString()})(window)` }]); ;
worldId === 0 ? w.webContents.executeJavaScript(`(${fn.toString()})(window)`) : w.webContents.executeJavaScriptInIsolatedWorld(worldId, [{ code: `(${fn.toString()})(window)` }]);
const getGCInfo = async (): Promise<{
trackedValues: number;
@@ -408,6 +408,17 @@ describe('contextBridge', () => {
expect(result).equal(true);
});
it('should proxy function arguments only once', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', (a: any, b: any) => a === b);
});
const result = await callWithBindings(async (root: any) => {
const obj = { foo: 1 };
return root.example(obj, obj);
});
expect(result).to.be.true();
});
it('should properly handle errors thrown in proxied functions', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', () => { throw new Error('oh no'); });
@@ -1290,6 +1301,131 @@ describe('contextBridge', () => {
});
});
});
describe('executeInMainWorld', () => {
it('serializes function and proxies args', async () => {
await makeBindingWindow(async () => {
const values = [
undefined,
null,
123,
'string',
true,
[123, 'string', true, ['foo']],
() => 'string',
Symbol('foo')
];
function appendArg (arg: any) {
// @ts-ignore
globalThis.args = globalThis.args || [];
// @ts-ignore
globalThis.args.push(arg);
}
for (const value of values) {
try {
await contextBridge.executeInMainWorld({
func: appendArg,
args: [value]
});
} catch {
contextBridge.executeInMainWorld({
func: appendArg,
args: ['FAIL']
});
}
}
});
const result = await callWithBindings(() => {
// @ts-ignore
return globalThis.args.map(arg => {
// Map unserializable IPC types to their type string
if (['function', 'symbol'].includes(typeof arg)) {
return typeof arg;
} else {
return arg;
}
});
});
expect(result).to.deep.equal([
undefined,
null,
123,
'string',
true,
[123, 'string', true, ['foo']],
'function',
'symbol'
]);
});
it('allows function args to be invoked', async () => {
const donePromise = once(ipcMain, 'done');
makeBindingWindow(() => {
const uuid = crypto.randomUUID();
const done = (receivedUuid: string) => {
if (receivedUuid === uuid) {
require('electron').ipcRenderer.send('done');
}
};
contextBridge.executeInMainWorld({
func: (callback, innerUuid) => {
callback(innerUuid);
},
args: [done, uuid]
});
});
await donePromise;
});
it('proxies arguments only once', async () => {
await makeBindingWindow(() => {
const obj = {};
// @ts-ignore
globalThis.result = contextBridge.executeInMainWorld({
func: (a, b) => a === b,
args: [obj, obj]
});
});
const result = await callWithBindings(() => {
// @ts-ignore
return globalThis.result;
}, 999);
expect(result).to.be.true();
});
it('safely clones returned objects', async () => {
await makeBindingWindow(() => {
const obj = contextBridge.executeInMainWorld({
func: () => ({})
});
// @ts-ignore
globalThis.safe = obj.constructor === Object;
});
const result = await callWithBindings(() => {
// @ts-ignore
return globalThis.safe;
}, 999);
expect(result).to.be.true();
});
it('uses internal Function.prototype.toString', async () => {
await makeBindingWindow(() => {
const funcHack = () => {
// @ts-ignore
globalThis.hacked = 'nope';
};
funcHack.toString = () => '() => { globalThis.hacked = \'gotem\'; }';
contextBridge.executeInMainWorld({
func: funcHack
});
});
const result = await callWithBindings(() => {
// @ts-ignore
return globalThis.hacked;
});
expect(result).to.equal('nope');
});
});
});
};

View File

@@ -0,0 +1,422 @@
import { ipcMain, session, webContents as webContentsModule, WebContents } from 'electron/main';
import { expect } from 'chai';
import { once, on } from 'node:events';
import * as fs from 'node:fs';
import * as http from 'node:http';
import * as path from 'node:path';
import { listen, waitUntil } from './lib/spec-helpers';
// Toggle to add extra debug output
const DEBUG = !process.env.CI;
describe('ServiceWorkerMain module', () => {
const fixtures = path.resolve(__dirname, 'fixtures');
const preloadRealmFixtures = path.resolve(fixtures, 'api/preload-realm');
const webContentsInternal: typeof ElectronInternal.WebContents = webContentsModule as any;
let ses: Electron.Session;
let serviceWorkers: Electron.ServiceWorkers;
let server: http.Server;
let baseUrl: string;
let wc: WebContents;
beforeEach(async () => {
ses = session.fromPartition(`service-worker-main-spec-${crypto.randomUUID()}`);
serviceWorkers = ses.serviceWorkers;
if (DEBUG) {
serviceWorkers.on('console-message', (_e, details) => {
console.log(details.message);
});
}
const uuid = crypto.randomUUID();
server = http.createServer((req, res) => {
const url = new URL(req.url!, `http://${req.headers.host}`);
// /{uuid}/{file}
const file = url.pathname!.split('/')[2]!;
if (file.endsWith('.js')) {
res.setHeader('Content-Type', 'application/javascript');
}
res.end(fs.readFileSync(path.resolve(fixtures, 'api', 'service-workers', file)));
});
const { port } = await listen(server);
baseUrl = `http://localhost:${port}/${uuid}`;
wc = webContentsInternal.create({ session: ses });
if (DEBUG) {
wc.on('console-message', (_e, _l, message) => {
console.log(message);
});
}
});
afterEach(async () => {
if (!wc.isDestroyed()) wc.destroy();
server.close();
ses.getPreloadScripts().map(({ id }) => ses.unregisterPreloadScript(id));
});
function registerPreload (scriptName: string) {
const id = ses.registerPreloadScript({
type: 'service-worker',
filePath: path.resolve(preloadRealmFixtures, scriptName)
});
expect(id).to.be.a('string');
}
async function loadWorkerScript (scriptUrl?: string) {
const scriptParams = scriptUrl ? `?scriptUrl=${scriptUrl}` : '';
return wc.loadURL(`${baseUrl}/index.html${scriptParams}`);
}
async function unregisterAllServiceWorkers () {
await wc.executeJavaScript(`(${async function () {
const registrations = await navigator.serviceWorker.getRegistrations();
for (const registration of registrations) {
registration.unregister();
}
}}())`);
}
async function waitForServiceWorker (expectedRunningStatus: Electron.ServiceWorkersRunningStatusChangedEventParams['runningStatus'] = 'starting') {
const serviceWorkerPromise = new Promise<Electron.ServiceWorkerMain>((resolve) => {
function onRunningStatusChanged ({ versionId, runningStatus }: Electron.ServiceWorkersRunningStatusChangedEventParams) {
if (runningStatus === expectedRunningStatus) {
const serviceWorker = serviceWorkers.getWorkerFromVersionID(versionId)!;
serviceWorkers.off('running-status-changed', onRunningStatusChanged);
resolve(serviceWorker);
}
}
serviceWorkers.on('running-status-changed', onRunningStatusChanged);
});
const serviceWorker = await serviceWorkerPromise;
expect(serviceWorker).to.not.be.undefined();
return serviceWorker!;
}
/** Runs a test using the framework in preload-tests.js */
const runTest = async (serviceWorker: Electron.ServiceWorkerMain, rpc: { name: string, args: any[] }) => {
const uuid = crypto.randomUUID();
serviceWorker.send('test', uuid, rpc.name, ...rpc.args);
return new Promise((resolve, reject) => {
serviceWorker.ipc.once(`test-result-${uuid}`, (_event, { error, result }) => {
if (error) {
reject(result);
} else {
resolve(result);
}
});
});
};
describe('serviceWorkers.getWorkerFromVersionID', () => {
it('returns undefined for non-live service worker', () => {
expect(serviceWorkers.getWorkerFromVersionID(-1)).to.be.undefined();
expect(serviceWorkers._getWorkerFromVersionIDIfExists(-1)).to.be.undefined();
});
it('returns instance for live service worker', async () => {
const runningStatusChanged = once(serviceWorkers, 'running-status-changed');
loadWorkerScript();
const [{ versionId }] = await runningStatusChanged;
const serviceWorker = serviceWorkers.getWorkerFromVersionID(versionId);
expect(serviceWorker).to.not.be.undefined();
const ifExistsServiceWorker = serviceWorkers._getWorkerFromVersionIDIfExists(versionId);
expect(ifExistsServiceWorker).to.not.be.undefined();
expect(serviceWorker).to.equal(ifExistsServiceWorker);
});
it('does not crash on script error', async () => {
wc.loadURL(`${baseUrl}/index.html?scriptUrl=sw-script-error.js`);
let serviceWorker;
const actualStatuses = [];
for await (const [{ versionId, runningStatus }] of on(serviceWorkers, 'running-status-changed')) {
if (!serviceWorker) {
serviceWorker = serviceWorkers.getWorkerFromVersionID(versionId);
}
actualStatuses.push(runningStatus);
if (runningStatus === 'stopping') {
break;
}
}
expect(actualStatuses).to.deep.equal(['starting', 'stopping']);
expect(serviceWorker).to.not.be.undefined();
});
it('does not find unregistered service worker', async () => {
loadWorkerScript();
const runningServiceWorker = await waitForServiceWorker('running');
const { versionId } = runningServiceWorker;
unregisterAllServiceWorkers();
await waitUntil(() => runningServiceWorker.isDestroyed());
const serviceWorker = serviceWorkers.getWorkerFromVersionID(versionId);
expect(serviceWorker).to.be.undefined();
});
});
describe('isDestroyed()', () => {
it('is not destroyed after being created', async () => {
loadWorkerScript();
const serviceWorker = await waitForServiceWorker();
expect(serviceWorker.isDestroyed()).to.be.false();
});
it('is destroyed after being unregistered', async () => {
loadWorkerScript();
const serviceWorker = await waitForServiceWorker();
expect(serviceWorker.isDestroyed()).to.be.false();
await unregisterAllServiceWorkers();
await waitUntil(() => serviceWorker.isDestroyed());
});
});
describe('"running-status-changed" event', () => {
it('handles when content::ServiceWorkerVersion has been destroyed', async () => {
loadWorkerScript('sw-unregister-self.js');
const serviceWorker = await waitForServiceWorker('running');
await waitUntil(() => serviceWorker.isDestroyed());
});
});
describe('startWorkerForScope()', () => {
it('resolves with running workers', async () => {
loadWorkerScript();
const serviceWorker = await waitForServiceWorker('running');
const startWorkerPromise = serviceWorkers.startWorkerForScope(serviceWorker.scope);
await expect(startWorkerPromise).to.eventually.be.fulfilled();
const otherSW = await startWorkerPromise;
expect(otherSW).to.equal(serviceWorker);
});
it('rejects with starting workers', async () => {
loadWorkerScript();
const serviceWorker = await waitForServiceWorker('starting');
const startWorkerPromise = serviceWorkers.startWorkerForScope(serviceWorker.scope);
await expect(startWorkerPromise).to.eventually.be.rejected();
});
it('starts previously stopped worker', async () => {
loadWorkerScript();
const serviceWorker = await waitForServiceWorker('running');
const { scope } = serviceWorker;
const stoppedPromise = waitForServiceWorker('stopped');
await serviceWorkers._stopAllWorkers();
await stoppedPromise;
const startWorkerPromise = serviceWorkers.startWorkerForScope(scope);
await expect(startWorkerPromise).to.eventually.be.fulfilled();
});
it('resolves when called twice', async () => {
loadWorkerScript();
const serviceWorker = await waitForServiceWorker('running');
const { scope } = serviceWorker;
const [swA, swB] = await Promise.all([
serviceWorkers.startWorkerForScope(scope),
serviceWorkers.startWorkerForScope(scope)
]);
expect(swA).to.equal(swB);
expect(swA).to.equal(serviceWorker);
});
});
describe('startTask()', () => {
it('has no tasks in-flight initially', async () => {
loadWorkerScript();
const serviceWorker = await waitForServiceWorker();
expect(serviceWorker._countExternalRequests()).to.equal(0);
});
it('can start and end a task', async () => {
loadWorkerScript();
// Internally, ServiceWorkerVersion buckets tasks into requests made
// during and after startup.
// ServiceWorkerContext::CountExternalRequestsForTest only considers
// requests made while SW is in running status so we need to wait for that
// to read an accurate count.
const serviceWorker = await waitForServiceWorker('running');
const task = serviceWorker.startTask();
expect(task).to.be.an('object');
expect(task).to.have.property('end').that.is.a('function');
expect(serviceWorker._countExternalRequests()).to.equal(1);
task.end();
// Count will decrement after Promise.finally callback
await new Promise<void>(queueMicrotask);
expect(serviceWorker._countExternalRequests()).to.equal(0);
});
it('can have more than one active task', async () => {
loadWorkerScript();
const serviceWorker = await waitForServiceWorker('running');
const taskA = serviceWorker.startTask();
const taskB = serviceWorker.startTask();
expect(serviceWorker._countExternalRequests()).to.equal(2);
taskB.end();
taskA.end();
// Count will decrement after Promise.finally callback
await new Promise<void>(queueMicrotask);
expect(serviceWorker._countExternalRequests()).to.equal(0);
});
it('throws when starting task after destroyed', async () => {
loadWorkerScript();
const serviceWorker = await waitForServiceWorker();
await unregisterAllServiceWorkers();
await waitUntil(() => serviceWorker.isDestroyed());
expect(() => serviceWorker.startTask()).to.throw();
});
it('throws when ending task after destroyed', async () => {
loadWorkerScript();
const serviceWorker = await waitForServiceWorker();
const task = serviceWorker.startTask();
await unregisterAllServiceWorkers();
await waitUntil(() => serviceWorker.isDestroyed());
expect(() => task.end()).to.throw();
});
});
describe("'versionId' property", () => {
it('matches the expected value', async () => {
const runningStatusChanged = once(serviceWorkers, 'running-status-changed');
wc.loadURL(`${baseUrl}/index.html`);
const [{ versionId }] = await runningStatusChanged;
const serviceWorker = serviceWorkers.getWorkerFromVersionID(versionId);
expect(serviceWorker).to.not.be.undefined();
if (!serviceWorker) return;
expect(serviceWorker).to.have.property('versionId').that.is.a('number');
expect(serviceWorker.versionId).to.equal(versionId);
});
});
describe("'scope' property", () => {
it('matches the expected value', async () => {
loadWorkerScript();
const serviceWorker = await waitForServiceWorker();
expect(serviceWorker).to.not.be.undefined();
if (!serviceWorker) return;
expect(serviceWorker).to.have.property('scope').that.is.a('string');
expect(serviceWorker.scope).to.equal(`${baseUrl}/`);
});
});
describe('ipc', () => {
beforeEach(() => {
registerPreload('preload-tests.js');
});
describe('on(channel)', () => {
it('can receive a message during startup', async () => {
registerPreload('preload-send-ping.js');
loadWorkerScript();
const serviceWorker = await waitForServiceWorker();
const pingPromise = once(serviceWorker.ipc, 'ping');
await pingPromise;
});
it('receives a message', async () => {
loadWorkerScript();
const serviceWorker = await waitForServiceWorker('running');
const pingPromise = once(serviceWorker.ipc, 'ping');
runTest(serviceWorker, { name: 'testSend', args: ['ping'] });
await pingPromise;
});
it('does not receive message on ipcMain', async () => {
loadWorkerScript();
const serviceWorker = await waitForServiceWorker('running');
const abortController = new AbortController();
try {
let pingReceived = false;
once(ipcMain, 'ping', { signal: abortController.signal }).then(() => {
pingReceived = true;
});
runTest(serviceWorker, { name: 'testSend', args: ['ping'] });
await once(ses, '-ipc-message');
await new Promise<void>(queueMicrotask);
expect(pingReceived).to.be.false();
} finally {
abortController.abort();
}
});
});
describe('handle(channel)', () => {
it('receives and responds to message', async () => {
loadWorkerScript();
const serviceWorker = await waitForServiceWorker('running');
serviceWorker.ipc.handle('ping', () => 'pong');
const result = await runTest(serviceWorker, { name: 'testInvoke', args: ['ping'] });
expect(result).to.equal('pong');
});
it('works after restarting worker', async () => {
loadWorkerScript();
const serviceWorker = await waitForServiceWorker('running');
const { scope } = serviceWorker;
serviceWorker.ipc.handle('ping', () => 'pong');
await serviceWorkers._stopAllWorkers();
await serviceWorkers.startWorkerForScope(scope);
const result = await runTest(serviceWorker, { name: 'testInvoke', args: ['ping'] });
expect(result).to.equal('pong');
});
});
});
describe('contextBridge', () => {
beforeEach(() => {
registerPreload('preload-tests.js');
});
it('can evaluate func from preload realm', async () => {
loadWorkerScript();
const serviceWorker = await waitForServiceWorker('running');
const result = await runTest(serviceWorker, { name: 'testEvaluate', args: ['evalConstructorName'] });
expect(result).to.equal('ServiceWorkerGlobalScope');
});
});
describe('extensions', () => {
const extensionFixtures = path.join(fixtures, 'extensions');
const testExtensionFixture = path.join(extensionFixtures, 'mv3-service-worker');
beforeEach(async () => {
ses = session.fromPartition(`persist:${crypto.randomUUID()}-service-worker-main-spec`);
serviceWorkers = ses.serviceWorkers;
});
it('can observe extension service workers', async () => {
const serviceWorkerPromise = waitForServiceWorker();
const extension = await ses.loadExtension(testExtensionFixture);
const serviceWorker = await serviceWorkerPromise;
expect(serviceWorker.scope).to.equal(extension.url);
});
it('has extension state available when preload runs', async () => {
registerPreload('preload-send-extension.js');
const serviceWorkerPromise = waitForServiceWorker();
const extensionPromise = ses.loadExtension(testExtensionFixture);
const serviceWorker = await serviceWorkerPromise;
const result = await new Promise<any>((resolve) => {
serviceWorker.ipc.handleOnce('preload-extension-result', (_event, result) => {
resolve(result);
});
});
const extension = await extensionPromise;
expect(result).to.be.an('object');
expect(result.id).to.equal(extension.id);
expect(result.manifest).to.deep.equal(result.manifest);
});
});
});

View File

@@ -27,8 +27,9 @@ describe('session.serviceWorkers', () => {
const uuid = v4();
server = http.createServer((req, res) => {
const url = new URL(req.url!, `http://${req.headers.host}`);
// /{uuid}/{file}
const file = req.url!.split('/')[2]!;
const file = url.pathname!.split('/')[2]!;
if (file.endsWith('.js')) {
res.setHeader('Content-Type', 'application/javascript');
@@ -76,7 +77,7 @@ describe('session.serviceWorkers', () => {
describe('console-message event', () => {
it('should correctly keep the source, message and level', async () => {
const messages: Record<string, Electron.MessageDetails> = {};
w.loadURL(`${baseUrl}/logs.html`);
w.loadURL(`${baseUrl}/index.html?scriptUrl=sw-logs.js`);
for await (const [, details] of on(ses.serviceWorkers, 'console-message')) {
messages[details.message] = details;
expect(details).to.have.property('source', 'console-api');

View File

@@ -0,0 +1,16 @@
const { contextBridge, ipcRenderer } = require('electron');
let result;
try {
result = contextBridge.executeInMainWorld({
func: () => ({
chromeType: typeof chrome,
id: globalThis.chrome?.runtime.id,
manifest: globalThis.chrome?.runtime.getManifest()
})
});
} catch (error) {
console.error(error);
}
ipcRenderer.invoke('preload-extension-result', result);

View File

@@ -0,0 +1,3 @@
const { ipcRenderer } = require('electron');
ipcRenderer.send('ping');

View File

@@ -0,0 +1,34 @@
const { contextBridge, ipcRenderer } = require('electron');
const evalTests = {
evalConstructorName: () => globalThis.constructor.name
};
const tests = {
testSend: (name, ...args) => {
ipcRenderer.send(name, ...args);
},
testInvoke: async (name, ...args) => {
const result = await ipcRenderer.invoke(name, ...args);
return result;
},
testEvaluate: (testName, args) => {
const func = evalTests[testName];
const result = args
? contextBridge.executeInMainWorld({ func, args })
: contextBridge.executeInMainWorld({ func });
return result;
}
};
ipcRenderer.on('test', async (_event, uuid, name, ...args) => {
console.debug(`running test ${name} for ${uuid}`);
try {
const result = await tests[name]?.(...args);
console.debug(`responding test ${name} for ${uuid}`);
ipcRenderer.send(`test-result-${uuid}`, { error: false, result });
} catch (error) {
console.debug(`erroring test ${name} for ${uuid}`);
ipcRenderer.send(`test-result-${uuid}`, { error: true, result: error.message });
}
});

View File

@@ -2,7 +2,8 @@
<html lang="en">
<body>
<script>
navigator.serviceWorker.register('sw.js', {
let scriptUrl = new URLSearchParams(location.search).get('scriptUrl') || 'sw.js';
navigator.serviceWorker.register(scriptUrl, {
scope: location.pathname.split('/').slice(0, 2).join('/') + '/'
})
</script>

View File

@@ -1,10 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<body>
<script>
navigator.serviceWorker.register('sw-logs.js', {
scope: location.pathname.split('/').slice(0, 2).join('/') + '/'
})
</script>
</body>
</html>

View File

@@ -0,0 +1 @@
throw new Error('service worker throwing on startup');

View File

@@ -0,0 +1,3 @@
self.addEventListener('install', function () {
registration.unregister();
});

View File

@@ -21,7 +21,7 @@ declare namespace NodeJS {
isComponentBuild(): boolean;
}
interface IpcRendererBinding {
interface IpcRendererImpl {
send(internal: boolean, channel: string, args: any[]): void;
sendSync(internal: boolean, channel: string, args: any[]): any;
sendToHost(channel: string, args: any[]): void;
@@ -29,6 +29,11 @@ declare namespace NodeJS {
postMessage(channel: string, message: any, transferables: MessagePort[]): void;
}
interface IpcRendererBinding {
createForRenderFrame(): IpcRendererImpl;
createForServiceWorker(): IpcRendererImpl;
}
interface V8UtilBinding {
getHiddenValue<T>(obj: any, key: string): T;
setHiddenValue<T>(obj: any, key: string, value: T): void;
@@ -111,6 +116,10 @@ declare namespace NodeJS {
setListeningForShutdown(listening: boolean): void;
}
interface ServiceWorkerMainBinding {
ServiceWorkerMain: typeof Electron.ServiceWorkerMain;
}
interface SessionBinding {
fromPartition: typeof Electron.Session.fromPartition,
fromPath: typeof Electron.Session.fromPath,
@@ -228,6 +237,7 @@ declare namespace NodeJS {
_linkedBinding(name: 'electron_browser_safe_storage'): { safeStorage: Electron.SafeStorage };
_linkedBinding(name: 'electron_browser_session'): SessionBinding;
_linkedBinding(name: 'electron_browser_screen'): { createScreen(): Electron.Screen };
_linkedBinding(name: 'electron_browser_service_worker_main'): ServiceWorkerMainBinding;
_linkedBinding(name: 'electron_browser_system_preferences'): { systemPreferences: Electron.SystemPreferences };
_linkedBinding(name: 'electron_browser_tray'): { Tray: Electron.Tray };
_linkedBinding(name: 'electron_browser_view'): { View: Electron.View };
@@ -235,7 +245,7 @@ declare namespace NodeJS {
_linkedBinding(name: 'electron_browser_web_view_manager'): WebViewManagerBinding;
_linkedBinding(name: 'electron_browser_web_frame_main'): WebFrameMainBinding;
_linkedBinding(name: 'electron_renderer_crash_reporter'): Electron.CrashReporter;
_linkedBinding(name: 'electron_renderer_ipc'): { ipc: IpcRendererBinding };
_linkedBinding(name: 'electron_renderer_ipc'): IpcRendererBinding;
_linkedBinding(name: 'electron_renderer_web_frame'): WebFrameBinding;
log: NodeJS.WriteStream['write'];
activateUvLoop(): void;

View File

@@ -63,10 +63,25 @@ declare namespace Electron {
overrideGlobalValueFromIsolatedWorld(keys: string[], value: any): void;
overrideGlobalValueWithDynamicPropsFromIsolatedWorld(keys: string[], value: any): void;
overrideGlobalPropertyFromIsolatedWorld(keys: string[], getter: Function, setter?: Function): void;
isInMainWorld(): boolean;
}
}
interface ServiceWorkers {
_getWorkerFromVersionIDIfExists(versionId: number): Electron.ServiceWorkerMain | undefined;
_stopAllWorkers(): Promise<void>;
}
interface ServiceWorkerMain {
_send(internal: boolean, channel: string, args: any): void;
_startExternalRequest(hasTimeout: boolean): { id: string, ok: boolean };
_finishExternalRequest(uuid: string): void;
_countExternalRequests(): number;
}
interface Session {
_init(): void;
}
interface TouchBar {
_removeFromWindow: (win: BaseWindow) => void;
}
@@ -76,7 +91,7 @@ declare namespace Electron {
getOwnerBrowserWindow(): Electron.BrowserWindow | null;
getLastWebPreferences(): Electron.WebPreferences | null;
_getProcessMemoryInfo(): Electron.ProcessMemoryInfo;
_getPreloadPaths(): string[];
_getPreloadScript(): Electron.PreloadScript | null;
equal(other: WebContents): boolean;
browserWindowOptions: BrowserWindowConstructorOptions;
_windowOpenHandler: ((details: Electron.HandlerDetails) => any) | null;
@@ -185,6 +200,14 @@ declare namespace Electron {
frameTreeNodeId?: number;
}
interface IpcMainServiceWorkerEvent {
_replyChannel: ReplyChannel;
}
interface IpcMainServiceWorkerInvokeEvent {
_replyChannel: ReplyChannel;
}
// Deprecated / undocumented BrowserWindow methods
interface BrowserWindow {
getURL(): string;
@@ -260,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;
}
@@ -330,6 +353,11 @@ declare namespace ElectronInternal {
class WebContents extends Electron.WebContents {
static create(opts?: Electron.WebPreferences): Electron.WebContents;
}
interface PreloadScript extends Electron.PreloadScript {
contents?: string;
error?: Error;
}
}
declare namespace Chrome {