mirror of
https://github.com/electron/electron.git
synced 2026-02-19 03:14:51 -05:00
Compare commits
9 Commits
remove-Ada
...
split-ipc-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c2d4f626a | ||
|
|
388469f6b5 | ||
|
|
c8aa24969e | ||
|
|
a5dc2bf113 | ||
|
|
0534f9f187 | ||
|
|
f59d8d618a | ||
|
|
6a730a8e80 | ||
|
|
e420260647 | ||
|
|
ed33b9ddca |
11
BUILD.gn
11
BUILD.gn
@@ -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",
|
||||
|
||||
6
build/webpack/webpack.config.preload_realm.js
Normal file
6
build/webpack/webpack.config.preload_realm.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = require('./webpack.config.base')({
|
||||
target: 'preload_realm',
|
||||
alwaysHasNode: false,
|
||||
wrapInitWithProfilingTimeout: true,
|
||||
wrapInitWithTryCatch: true
|
||||
});
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
75
docs/api/ipc-main-service-worker.md
Normal file
75
docs/api/ipc-main-service-worker.md
Normal 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
|
||||
@@ -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
|
||||
|
||||
|
||||
75
docs/api/service-worker-main.md
Normal file
75
docs/api/service-worker-main.md
Normal 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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
@@ -1330,18 +1330,44 @@ the initial state will be `interrupted`. The download will start only when the
|
||||
|
||||
Returns `Promise<void>` - resolves when the session’s 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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
11
docs/api/structures/ipc-main-service-worker-event.md
Normal file
11
docs/api/structures/ipc-main-service-worker-event.md
Normal 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[]
|
||||
@@ -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.
|
||||
6
docs/api/structures/preload-script-registration.md
Normal file
6
docs/api/structures/preload-script-registration.md
Normal 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.
|
||||
6
docs/api/structures/preload-script.md
Normal file
6
docs/api/structures/preload-script.md
Normal 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.
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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') },
|
||||
|
||||
39
lib/browser/api/service-worker-main.ts
Normal file
39
lib/browser/api/service-worker-main.ts
Normal 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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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[]) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
91
lib/browser/ipc-dispatch.ts
Normal file
91
lib/browser/ipc-dispatch.ts
Normal 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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
18
lib/preload_realm/.eslintrc.json
Normal file
18
lib/preload_realm/.eslintrc.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"rules": {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"paths": [
|
||||
"electron",
|
||||
"electron/main"
|
||||
],
|
||||
"patterns": [
|
||||
"./*",
|
||||
"../*",
|
||||
"@electron/internal/browser/*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
6
lib/preload_realm/api/exports/electron.ts
Normal file
6
lib/preload_realm/api/exports/electron.ts
Normal 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);
|
||||
14
lib/preload_realm/api/module-list.ts
Normal file
14
lib/preload_realm/api/module-list.ts
Normal 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
56
lib/preload_realm/init.ts
Normal 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);
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:': {
|
||||
|
||||
14
lib/renderer/ipc-native-setup.ts
Normal file
14
lib/renderer/ipc-native-setup.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
17
lib/renderer/ipc-renderer-bindings.ts
Normal file
17
lib/renderer/ipc-renderer-bindings.ts
Normal 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`);
|
||||
}
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
30
lib/sandboxed_renderer/pre-init.ts
Normal file
30
lib/sandboxed_renderer/pre-init.ts
Normal 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);
|
||||
107
lib/sandboxed_renderer/preload.ts
Normal file
107
lib/sandboxed_renderer/preload.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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};
|
||||
};
|
||||
|
||||
|
||||
319
shell/browser/api/electron_api_service_worker_main.cc
Normal file
319
shell/browser/api/electron_api_service_worker_main.cc
Normal 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)
|
||||
174
shell/browser/api/electron_api_service_worker_main.h
Normal file
174
shell/browser/api/electron_api_service_worker_main.h
Normal 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_
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
89
shell/browser/api/ipc_dispatcher.h
Normal file
89
shell/browser/api/ipc_dispatcher.h
Normal 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_
|
||||
199
shell/browser/electron_api_sw_ipc_handler_impl.cc
Normal file
199
shell/browser/electron_api_sw_ipc_handler_impl.cc
Normal 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
|
||||
98
shell/browser/electron_api_sw_ipc_handler_impl.h
Normal file
98
shell/browser/electron_api_sw_ipc_handler_impl.h
Normal 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_
|
||||
@@ -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,
|
||||
|
||||
104
shell/browser/preload_script.h
Normal file
104
shell/browser/preload_script.h
Normal 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_
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
25
shell/common/gin_converters/service_worker_converter.cc
Normal file
25
shell/common/gin_converters/service_worker_converter.cc
Normal 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
|
||||
21
shell/common/gin_converters/service_worker_converter.h
Normal file
21
shell/common/gin_converters/service_worker_converter.h
Normal 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_
|
||||
@@ -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,
|
||||
|
||||
66
shell/common/gin_helper/reply_channel.cc
Normal file
66
shell/common/gin_helper/reply_channel.cc
Normal 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
|
||||
54
shell/common/gin_helper/reply_channel.h
Normal file
54
shell/common/gin_helper/reply_channel.h
Normal 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_
|
||||
@@ -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) \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
84
shell/renderer/electron_ipc_native.cc
Normal file
84
shell/renderer/electron_ipc_native.cc
Normal 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
|
||||
22
shell/renderer/electron_ipc_native.h
Normal file
22
shell/renderer/electron_ipc_native.h
Normal 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_
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
295
shell/renderer/preload_realm_context.cc
Normal file
295
shell/renderer/preload_realm_context.cc
Normal 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
|
||||
34
shell/renderer/preload_realm_context.h
Normal file
34
shell/renderer/preload_realm_context.h
Normal 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_
|
||||
80
shell/renderer/preload_utils.cc
Normal file
80
shell/renderer/preload_utils.cc
Normal 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
|
||||
27
shell/renderer/preload_utils.h
Normal file
27
shell/renderer/preload_utils.h
Normal 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_
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
72
shell/renderer/service_worker_data.cc
Normal file
72
shell/renderer/service_worker_data.cc
Normal 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
|
||||
68
shell/renderer/service_worker_data.h
Normal file
68
shell/renderer/service_worker_data.h
Normal 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_
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
422
spec/api-service-worker-main-spec.ts
Normal file
422
spec/api-service-worker-main-spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
|
||||
16
spec/fixtures/api/preload-realm/preload-send-extension.js
vendored
Normal file
16
spec/fixtures/api/preload-realm/preload-send-extension.js
vendored
Normal 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);
|
||||
3
spec/fixtures/api/preload-realm/preload-send-ping.js
vendored
Normal file
3
spec/fixtures/api/preload-realm/preload-send-ping.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
const { ipcRenderer } = require('electron');
|
||||
|
||||
ipcRenderer.send('ping');
|
||||
34
spec/fixtures/api/preload-realm/preload-tests.js
vendored
Normal file
34
spec/fixtures/api/preload-realm/preload-tests.js
vendored
Normal 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 });
|
||||
}
|
||||
});
|
||||
3
spec/fixtures/api/service-workers/index.html
vendored
3
spec/fixtures/api/service-workers/index.html
vendored
@@ -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>
|
||||
|
||||
10
spec/fixtures/api/service-workers/logs.html
vendored
10
spec/fixtures/api/service-workers/logs.html
vendored
@@ -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>
|
||||
1
spec/fixtures/api/service-workers/sw-script-error.js
vendored
Normal file
1
spec/fixtures/api/service-workers/sw-script-error.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
throw new Error('service worker throwing on startup');
|
||||
3
spec/fixtures/api/service-workers/sw-unregister-self.js
vendored
Normal file
3
spec/fixtures/api/service-workers/sw-unregister-self.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
self.addEventListener('install', function () {
|
||||
registration.unregister();
|
||||
});
|
||||
14
typings/internal-ambient.d.ts
vendored
14
typings/internal-ambient.d.ts
vendored
@@ -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;
|
||||
|
||||
38
typings/internal-electron.d.ts
vendored
38
typings/internal-electron.d.ts
vendored
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user