mirror of
https://github.com/electron/electron.git
synced 2026-02-26 03:01:17 -05:00
Compare commits
27 Commits
test/osr-c
...
v35.0.0-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc6164fe27 | ||
|
|
59e4794ff5 | ||
|
|
fa03b92f7e | ||
|
|
9d696ceffe | ||
|
|
e9b3e4cc91 | ||
|
|
895bc51272 | ||
|
|
ef1ad85082 | ||
|
|
e99328a45e | ||
|
|
04f5fe6a1c | ||
|
|
08b6bb1712 | ||
|
|
813efbcdf7 | ||
|
|
d34fa2e301 | ||
|
|
724744af16 | ||
|
|
91bb748eaa | ||
|
|
d77c2d75ed | ||
|
|
f4c3eb4391 | ||
|
|
9aca9e9fb6 | ||
|
|
e0fa647601 | ||
|
|
6b0fd02c0a | ||
|
|
1017ac821f | ||
|
|
91b53b633a | ||
|
|
8da9572592 | ||
|
|
291bbff5d8 | ||
|
|
d704a3fc5b | ||
|
|
fb70b81ee6 | ||
|
|
97fa059e1f | ||
|
|
8a64cdc0b1 |
@@ -16,5 +16,5 @@ runs:
|
||||
e auto-update disable
|
||||
if [ "$(expr substr $(uname -s) 1 10)" == "MSYS_NT-10" ]; then
|
||||
e d cipd.bat --version
|
||||
cp "C:\Python37\python.exe" "C:\Python37\python3.exe"
|
||||
cp "C:\Python311\python.exe" "C:\Python311\python3.exe"
|
||||
fi
|
||||
|
||||
2
.github/workflows/archaeologist-dig.yml
vendored
2
.github/workflows/archaeologist-dig.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
||||
sha-file: .dig-old
|
||||
filename: electron.old.d.ts
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 #v4.6.0
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b #v4.5.0
|
||||
with:
|
||||
name: artifacts
|
||||
path: electron/artifacts
|
||||
|
||||
6
.github/workflows/branch-created.yml
vendored
6
.github/workflows/branch-created.yml
vendored
@@ -94,7 +94,7 @@ jobs:
|
||||
}))
|
||||
- name: Create Release Project Board
|
||||
if: ${{ steps.check-major-version.outputs.MAJOR }}
|
||||
uses: dsanders11/project-actions/copy-project@9c80cd31f58599941c64f74636bea95ba5d46090 # v1.5.1
|
||||
uses: dsanders11/project-actions/copy-project@438b25e007c2f4efec324497fadc6402e7cc61a6 # v1.4.0
|
||||
id: create-release-board
|
||||
with:
|
||||
drafts: true
|
||||
@@ -114,14 +114,14 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||
- name: Find Previous Release Project Board
|
||||
if: ${{ steps.check-major-version.outputs.MAJOR }}
|
||||
uses: dsanders11/project-actions/find-project@9c80cd31f58599941c64f74636bea95ba5d46090 # v1.5.1
|
||||
uses: dsanders11/project-actions/find-project@438b25e007c2f4efec324497fadc6402e7cc61a6 # v1.4.0
|
||||
id: find-prev-release-board
|
||||
with:
|
||||
title: ${{ steps.generate-project-metadata.outputs.prev-prev-major }}-x-y
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
- name: Close Previous Release Project Board
|
||||
if: ${{ steps.check-major-version.outputs.MAJOR }}
|
||||
uses: dsanders11/project-actions/close-project@9c80cd31f58599941c64f74636bea95ba5d46090 # v1.5.1
|
||||
uses: dsanders11/project-actions/close-project@438b25e007c2f4efec324497fadc6402e7cc61a6 # v1.4.0
|
||||
with:
|
||||
project-number: ${{ steps.find-prev-release-board.outputs.number }}
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
6
.github/workflows/issue-labeled.yml
vendored
6
.github/workflows/issue-labeled.yml
vendored
@@ -20,13 +20,12 @@ jobs:
|
||||
creds: ${{ secrets.ISSUE_TRIAGE_GH_APP_CREDS }}
|
||||
org: electron
|
||||
- name: Set status
|
||||
uses: dsanders11/project-actions/edit-item@9c80cd31f58599941c64f74636bea95ba5d46090 # v1.5.1
|
||||
uses: dsanders11/project-actions/edit-item@438b25e007c2f4efec324497fadc6402e7cc61a6 # v1.4.0
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
project-number: 90
|
||||
field: Status
|
||||
field-value: ✅ Triaged
|
||||
fail-if-item-not-found: false
|
||||
issue-labeled-blocked:
|
||||
name: blocked/* label added
|
||||
if: startsWith(github.event.label.name, 'blocked/')
|
||||
@@ -39,13 +38,12 @@ jobs:
|
||||
creds: ${{ secrets.ISSUE_TRIAGE_GH_APP_CREDS }}
|
||||
org: electron
|
||||
- name: Set status
|
||||
uses: dsanders11/project-actions/edit-item@9c80cd31f58599941c64f74636bea95ba5d46090 # v1.5.1
|
||||
uses: dsanders11/project-actions/edit-item@438b25e007c2f4efec324497fadc6402e7cc61a6 # v1.4.0
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
project-number: 90
|
||||
field: Status
|
||||
field-value: 🛑 Blocked
|
||||
fail-if-item-not-found: false
|
||||
issue-labeled-blocked-need-repro:
|
||||
name: blocked/need-repro label added
|
||||
if: github.event.label.name == 'blocked/need-repro'
|
||||
|
||||
2
.github/workflows/issue-opened.yml
vendored
2
.github/workflows/issue-opened.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
creds: ${{ secrets.ISSUE_TRIAGE_GH_APP_CREDS }}
|
||||
org: electron
|
||||
- name: Add to Issue Triage
|
||||
uses: dsanders11/project-actions/add-item@9c80cd31f58599941c64f74636bea95ba5d46090 # v1.5.1
|
||||
uses: dsanders11/project-actions/add-item@438b25e007c2f4efec324497fadc6402e7cc61a6 # v1.4.0
|
||||
with:
|
||||
field: Reporter
|
||||
field-value: ${{ github.event.issue.user.login }}
|
||||
|
||||
5
.github/workflows/issue-transferred.yml
vendored
5
.github/workflows/issue-transferred.yml
vendored
@@ -10,7 +10,6 @@ jobs:
|
||||
issue-transferred:
|
||||
name: Issue Transferred
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ !github.event.changes.new_repository.private }}
|
||||
steps:
|
||||
- name: Generate GitHub App token
|
||||
uses: electron/github-app-auth-action@384fd19694fe7b6dcc9a684746c6976ad78228ae # v1.1.1
|
||||
@@ -19,9 +18,7 @@ jobs:
|
||||
creds: ${{ secrets.ISSUE_TRIAGE_GH_APP_CREDS }}
|
||||
org: electron
|
||||
- name: Remove from issue triage
|
||||
uses: dsanders11/project-actions/delete-item@9c80cd31f58599941c64f74636bea95ba5d46090 # v1.5.1
|
||||
uses: dsanders11/project-actions/delete-item@438b25e007c2f4efec324497fadc6402e7cc61a6 # v1.4.0
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
project-number: 90
|
||||
item: ${{ github.event.changes.new_issue.html_url }}
|
||||
fail-if-item-not-found: false
|
||||
|
||||
3
.github/workflows/issue-unlabeled.yml
vendored
3
.github/workflows/issue-unlabeled.yml
vendored
@@ -30,10 +30,9 @@ jobs:
|
||||
org: electron
|
||||
- name: Set status
|
||||
if: ${{ steps.check-for-blocked-labels.outputs.NOT_BLOCKED }}
|
||||
uses: dsanders11/project-actions/edit-item@9c80cd31f58599941c64f74636bea95ba5d46090 # v1.5.1
|
||||
uses: dsanders11/project-actions/edit-item@438b25e007c2f4efec324497fadc6402e7cc61a6 # v1.4.0
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
project-number: 90
|
||||
field: Status
|
||||
field-value: 📥 Was Blocked
|
||||
fail-if-item-not-found: false
|
||||
|
||||
@@ -56,8 +56,8 @@ on:
|
||||
default: false
|
||||
|
||||
concurrency:
|
||||
group: electron-build-and-test-and-nan-${{ inputs.target-platform }}-${{ inputs.target-arch }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && !endsWith(github.ref, '-x-y') }}
|
||||
group: electron-build-and-test-and-nan-${{ inputs.target-platform }}-${{ inputs.target-arch }}-${{ github.ref_protected == true && github.run_id || github.ref }}
|
||||
cancel-in-progress: ${{ github.ref_protected != true }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
@@ -56,8 +56,8 @@ on:
|
||||
default: false
|
||||
|
||||
concurrency:
|
||||
group: electron-build-and-test-${{ inputs.target-platform }}-${{ inputs.target-arch }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && !endsWith(github.ref, '-x-y') }}
|
||||
group: electron-build-and-test-${{ inputs.target-platform }}-${{ inputs.target-arch }}-${{ github.ref_protected == true && github.run_id || github.ref }}
|
||||
cancel-in-progress: ${{ github.ref_protected != true }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
4
.github/workflows/pipeline-electron-lint.yml
vendored
4
.github/workflows/pipeline-electron-lint.yml
vendored
@@ -9,8 +9,8 @@ on:
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: electron-lint-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && !endsWith(github.ref, '-x-y') }}
|
||||
group: electron-lint-${{ github.ref_protected == true && github.run_id || github.ref }}
|
||||
cancel-in-progress: ${{ github.ref_protected != true }}
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
|
||||
@@ -61,8 +61,8 @@ on:
|
||||
|
||||
|
||||
concurrency:
|
||||
group: electron-build-${{ inputs.target-platform }}-${{ inputs.target-arch }}-${{ inputs.target-variant }}-${{ inputs.is-asan }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && !endsWith(github.ref, '-x-y') }}
|
||||
group: electron-build-${{ inputs.target-platform }}-${{ inputs.target-arch }}-${{ inputs.target-variant }}-${{ inputs.is-asan }}-${{ github.ref_protected == true && github.run_id || github.ref }}
|
||||
cancel-in-progress: ${{ github.ref_protected != true }}
|
||||
|
||||
env:
|
||||
ELECTRON_ARTIFACTS_BLOB_STORAGE: ${{ secrets.ELECTRON_ARTIFACTS_BLOB_STORAGE }}
|
||||
|
||||
@@ -27,8 +27,8 @@ on:
|
||||
default: false
|
||||
|
||||
concurrency:
|
||||
group: electron-test-${{ inputs.target-platform }}-${{ inputs.target-arch }}-${{ inputs.is-asan }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && !endsWith(github.ref, '-x-y') }}
|
||||
group: electron-test-${{ inputs.target-platform }}-${{ inputs.target-arch }}-${{ inputs.is-asan }}-${{ github.ref_protected == true && github.run_id || github.ref }}
|
||||
cancel-in-progress: ${{ github.ref_protected != true }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -239,7 +239,7 @@ jobs:
|
||||
if: always() && !cancelled()
|
||||
- name: Upload Test Artifacts
|
||||
if: always() && !cancelled()
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b
|
||||
with:
|
||||
name: test_artifacts_${{ env.ARTIFACT_KEY }}_${{ matrix.shard }}
|
||||
path: src/electron/spec/artifacts
|
||||
|
||||
@@ -27,8 +27,8 @@ on:
|
||||
default: testing
|
||||
|
||||
concurrency:
|
||||
group: electron-node-nan-test-${{ inputs.target-platform }}-${{ inputs.target-arch }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && !endsWith(github.ref, '-x-y') }}
|
||||
group: electron-node-nan-test-${{ inputs.target-platform }}-${{ inputs.target-arch }}-${{ github.ref_protected == true && github.run_id || github.ref }}
|
||||
cancel-in-progress: ${{ github.ref_protected != true }}
|
||||
|
||||
env:
|
||||
ELECTRON_OUT_DIR: Default
|
||||
|
||||
6
.github/workflows/pull-request-labeled.yml
vendored
6
.github/workflows/pull-request-labeled.yml
vendored
@@ -15,12 +15,12 @@ jobs:
|
||||
- name: Trigger Slack workflow
|
||||
uses: slackapi/slack-github-action@485a9d42d3a73031f12ec201c457e2162c45d02d # v2.0.0
|
||||
with:
|
||||
webhook: ${{ secrets.BACKPORT_REQUESTED_SLACK_WEBHOOK_URL }}
|
||||
webhook-type: webhook-trigger
|
||||
payload: |
|
||||
{
|
||||
"url": "${{ github.event.pull_request.html_url }}"
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.BACKPORT_REQUESTED_SLACK_WEBHOOK_URL }}
|
||||
pull-request-labeled-deprecation-review-complete:
|
||||
name: deprecation-review/complete label added
|
||||
if: github.event.label.name == 'deprecation-review/complete ✅'
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
creds: ${{ secrets.RELEASE_BOARD_GH_APP_CREDS }}
|
||||
org: electron
|
||||
- name: Set status
|
||||
uses: dsanders11/project-actions/edit-item@9c80cd31f58599941c64f74636bea95ba5d46090 # v1.5.1
|
||||
uses: dsanders11/project-actions/edit-item@438b25e007c2f4efec324497fadc6402e7cc61a6 # v1.4.0
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
project-number: 94
|
||||
|
||||
4
.github/workflows/scorecards.yml
vendored
4
.github/workflows/scorecards.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
||||
# format to the repository Actions tab.
|
||||
- name: "Upload artifact"
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
@@ -50,6 +50,6 @@ jobs:
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1
|
||||
uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
2
.github/workflows/stable-prep-items.yml
vendored
2
.github/workflows/stable-prep-items.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
PROJECT_NUMBER=$(gh project list --owner electron --format json | jq -r '.projects | map(select(.title | test("^[0-9]+-x-y$"))) | max_by(.number) | .number')
|
||||
echo "PROJECT_NUMBER=$PROJECT_NUMBER" >> "$GITHUB_OUTPUT"
|
||||
- name: Update Completed Stable Prep Items
|
||||
uses: dsanders11/project-actions/completed-by@9c80cd31f58599941c64f74636bea95ba5d46090 # v1.5.1
|
||||
uses: dsanders11/project-actions/completed-by@438b25e007c2f4efec324497fadc6402e7cc61a6 # v1.4.0
|
||||
with:
|
||||
field: Prep Status
|
||||
field-value: ✅ Complete
|
||||
|
||||
2
.github/workflows/update_appveyor_image.yml
vendored
2
.github/workflows/update_appveyor_image.yml
vendored
@@ -44,7 +44,7 @@ jobs:
|
||||
fi
|
||||
- name: (Optionally) Update Appveyor Image
|
||||
if: ${{ env.APPVEYOR_IMAGE_VERSION }}
|
||||
uses: mikefarah/yq@8bf425b4d1344db7cd469a8d10a390876e0c77fd # v4.45.1
|
||||
uses: mikefarah/yq@4839dbbf80445070a31c7a9c1055da527db2d5ee # v4.44.6
|
||||
with:
|
||||
cmd: |
|
||||
yq '.image = "${{ env.APPVEYOR_IMAGE_VERSION }}"' "appveyor.yml" > "appveyor2.yml"
|
||||
|
||||
@@ -127,6 +127,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
|
||||
|
||||
@@ -12,17 +12,9 @@ shortcuts.
|
||||
not have the keyboard focus. This module cannot be used before the `ready`
|
||||
event of the app module is emitted.
|
||||
|
||||
Please also note that it is also possible to use Chromium's
|
||||
`GlobalShortcutsPortal` implementation, which allows apps to bind global
|
||||
shortcuts when running within a Wayland session.
|
||||
|
||||
```js
|
||||
const { app, globalShortcut } = require('electron')
|
||||
|
||||
// Enable usage of Portal's globalShortcuts. This is essential for cases when
|
||||
// the app runs in a Wayland session.
|
||||
app.commandLine.appendSwitch('enable-features', 'GlobalShortcutsPortal')
|
||||
|
||||
app.whenReady().then(() => {
|
||||
// Register a 'CommandOrControl+X' shortcut listener.
|
||||
const ret = globalShortcut.register('CommandOrControl+X', () => {
|
||||
|
||||
@@ -41,6 +41,16 @@ The `ipcRenderer` module has the following method to listen for events and send
|
||||
Listens to `channel`, when a new message arrives `listener` would be called with
|
||||
`listener(event, args...)`.
|
||||
|
||||
:::warning
|
||||
Do not expose the `event` argument to the renderer for security reasons! Wrap any
|
||||
callback that you receive from the renderer in another function like this:
|
||||
`ipcRenderer.on('my-channel', (event, ...args) => callback(...args))`.
|
||||
Not wrapping the callback in such a function would expose dangerous Electron APIs
|
||||
to the renderer process. See the
|
||||
[security guide](../tutorial/security.md#20-do-not-expose-electron-apis-to-untrusted-web-content)
|
||||
for more info.
|
||||
:::
|
||||
|
||||
### `ipcRenderer.off(channel, listener)`
|
||||
|
||||
* `channel` string
|
||||
|
||||
34
docs/api/service-worker-main.md
Normal file
34
docs/api/service-worker-main.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# 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.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.
|
||||
|
||||
### Instance Properties
|
||||
|
||||
#### `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.
|
||||
@@ -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,56 @@ 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. If there's no associated version, or its running status has changed to 'stopped', this will return `undefined`.
|
||||
|
||||
#### `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.
|
||||
|
||||
<!-- TODO(samuelmaddock): extend example to send IPC after starting worker -->
|
||||
|
||||
```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
|
||||
await serviceWorkers.startWorkerForScope(scope)
|
||||
} catch (error) {
|
||||
console.error(`Failed to start service worker for ${scope}`)
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
@@ -1330,18 +1330,43 @@ 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.
|
||||
|
||||
#### `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.
|
||||
|
||||
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`.
|
||||
* `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`.
|
||||
* `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
|
||||
|
||||
@@ -14,6 +14,40 @@ This document uses the following convention to categorize breaking changes:
|
||||
|
||||
## 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 beyond `frame`.
|
||||
|
||||
```js
|
||||
// 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)
|
||||
```
|
||||
|
||||
### Deprecated: `level`, `message`, `line`, and `sourceId` arguments in `console-message` event on `WebContents`
|
||||
|
||||
The `console-message` event on `WebContents` has been updated to provide details on the `Event`
|
||||
@@ -56,15 +90,15 @@ immediately upon being received. Otherwise, it's not guaranteed to point to the
|
||||
same webpage as when received. To avoid misaligned expectations, Electron will
|
||||
return `null` in the case of late access where the webpage has changed.
|
||||
|
||||
```ts
|
||||
```js
|
||||
ipcMain.on('unload-event', (event) => {
|
||||
event.senderFrame; // ✅ accessed immediately
|
||||
});
|
||||
event.senderFrame // ✅ accessed immediately
|
||||
})
|
||||
|
||||
ipcMain.on('unload-event', async (event) => {
|
||||
await crossOriginNavigationPromise;
|
||||
event.senderFrame; // ❌ returns `null` due to late access
|
||||
});
|
||||
await crossOriginNavigationPromise
|
||||
event.senderFrame // ❌ returns `null` due to late access
|
||||
})
|
||||
```
|
||||
|
||||
### Behavior Changed: custom protocol URL handling on Windows
|
||||
|
||||
@@ -22,12 +22,9 @@ In `preload.js` use the [`contextBridge`][] to inject a method `window.electron.
|
||||
|
||||
```js
|
||||
const { contextBridge, ipcRenderer } = require('electron')
|
||||
const path = require('node:path')
|
||||
|
||||
contextBridge.exposeInMainWorld('electron', {
|
||||
startDrag: (fileName) => {
|
||||
ipcRenderer.send('ondragstart', path.join(process.cwd(), fileName))
|
||||
}
|
||||
startDrag: (fileName) => ipcRenderer.send('ondragstart', fileName)
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -116,6 +116,7 @@ You should at least follow these steps to improve the security of your applicati
|
||||
17. [Validate the `sender` of all IPC messages](#17-validate-the-sender-of-all-ipc-messages)
|
||||
18. [Avoid usage of the `file://` protocol and prefer usage of custom protocols](#18-avoid-usage-of-the-file-protocol-and-prefer-usage-of-custom-protocols)
|
||||
19. [Check which fuses you can change](#19-check-which-fuses-you-can-change)
|
||||
20. [Do not expose Electron APIs to untrusted web content](#20-do-not-expose-electron-apis-to-untrusted-web-content)
|
||||
|
||||
To automate the detection of misconfigurations and insecure patterns, it is
|
||||
possible to use
|
||||
@@ -229,7 +230,7 @@ API to remotely loaded content via the [contextBridge API](../api/context-bridge
|
||||
### 3. Enable Context Isolation
|
||||
|
||||
:::info
|
||||
This recommendation is the default behavior in Electron since 12.0.0.
|
||||
Context Isolation is the default behavior in Electron since 12.0.0.
|
||||
:::
|
||||
|
||||
Context isolation is an Electron feature that allows developers to run code
|
||||
@@ -804,6 +805,48 @@ flipping these fuses easy. Check out the README of that module for more details
|
||||
potential error cases, and refer to
|
||||
[How do I flip the fuses?](./fuses.md#how-do-i-flip-the-fuses) in our documentation.
|
||||
|
||||
### 20. Do not expose Electron APIs to untrusted web content
|
||||
|
||||
You should not directly expose Electron's APIs, especially IPC, to untrusted web content in your
|
||||
preload scripts.
|
||||
|
||||
### Why?
|
||||
|
||||
Exposing raw APIs like `ipcRenderer.on` is dangerous because it gives renderer processes direct
|
||||
access to the entire IPC event system, allowing them to listen for any IPC events, not just the ones
|
||||
intended for them.
|
||||
|
||||
To avoid that exposure, we also cannot pass callbacks directly through: The first
|
||||
argument to IPC event callbacks is an `IpcRendererEvent` object, which includes properties like `sender`
|
||||
that provide access to the underlying `ipcRenderer` instance. Even if you only listen for specific
|
||||
events, passing the callback directly means the renderer gets access to this event object.
|
||||
|
||||
In short, we want the untrusted web content to only have access to necessary information and APIs.
|
||||
|
||||
### How?
|
||||
|
||||
```js title='preload'.js'
|
||||
// Bad
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
on: ipcRenderer.on
|
||||
})
|
||||
|
||||
// Also bad
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
onUpdateCounter: (callback) => ipcRenderer.on('update-counter', callback)
|
||||
})
|
||||
|
||||
// Good
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
onUpdateCounter: (callback) => ipcRenderer.on('update-counter', (_event, value) => callback(value))
|
||||
})
|
||||
```
|
||||
|
||||
:::info
|
||||
For more information on what `contextIsolation` is and how to use it to secure your app,
|
||||
please see the [Context Isolation](context-isolation.md) document.
|
||||
:::
|
||||
|
||||
[breaking-changes]: ../breaking-changes.md
|
||||
[browser-window]: ../api/browser-window.md
|
||||
[webview-tag]: ../api/webview-tag.md
|
||||
|
||||
@@ -45,6 +45,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",
|
||||
@@ -114,6 +115,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",
|
||||
@@ -183,6 +186,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 +244,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",
|
||||
|
||||
@@ -296,6 +296,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",
|
||||
@@ -474,6 +476,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",
|
||||
@@ -613,6 +616,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",
|
||||
|
||||
@@ -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') },
|
||||
|
||||
17
lib/browser/api/service-worker-main.ts
Normal file
17
lib/browser/api/service-worker-main.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
const { ServiceWorkerMain } = process._linkedBinding('electron_browser_service_worker_main');
|
||||
|
||||
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,5 @@
|
||||
import { fetchWithSession } from '@electron/internal/browser/api/net-fetch';
|
||||
import * as deprecate from '@electron/internal/common/deprecate';
|
||||
|
||||
import { net } from 'electron/main';
|
||||
|
||||
@@ -36,6 +37,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,
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ 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) {
|
||||
@@ -43,22 +44,40 @@ 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;
|
||||
const getPreloadScriptsFromEvent = (event: ElectronInternal.IpcMainInternalEvent) => {
|
||||
const session: Electron.Session = event.sender.session;
|
||||
const preloadScripts = session.getPreloadScripts();
|
||||
const framePreloads = preloadScripts.filter(script => script.type === 'frame');
|
||||
|
||||
const webPrefPreload = event.sender._getPreloadScript();
|
||||
if (webPrefPreload) framePreloads.push(webPrefPreload);
|
||||
|
||||
// TODO(samuelmaddock): Remove filter after Session.setPreloads is fully
|
||||
// deprecated. The new API will prevent relative paths from being registered.
|
||||
return framePreloads.filter(script => path.isAbsolute(script.filePath));
|
||||
};
|
||||
|
||||
const readPreloadScript = async function (script: Electron.PreloadScript): Promise<ElectronInternal.PreloadScript> {
|
||||
let contents;
|
||||
let error;
|
||||
try {
|
||||
preloadSrc = await fs.promises.readFile(preloadPath, 'utf8');
|
||||
} catch (error) {
|
||||
preloadError = error;
|
||||
contents = await fs.promises.readFile(script.filePath, 'utf8');
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
error = err;
|
||||
}
|
||||
}
|
||||
return { preloadPath, preloadSrc, preloadError };
|
||||
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,7 +90,8 @@ 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) {
|
||||
|
||||
@@ -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,6 +1,7 @@
|
||||
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';
|
||||
@@ -11,35 +12,14 @@ declare const binding: {
|
||||
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,8 +40,7 @@ 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
|
||||
v8Util.setHiddenValue(global, 'emit-process-event', (event: string) => {
|
||||
@@ -72,77 +51,22 @@ v8Util.setHiddenValue(global, 'emit-process-event', (event: string) => {
|
||||
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;
|
||||
}
|
||||
|
||||
// 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,
|
||||
loadableModules,
|
||||
process: preloadProcess,
|
||||
createPreloadScript: binding.createPreloadScript,
|
||||
exposeGlobals: {
|
||||
Buffer,
|
||||
// FIXME(samuelmaddock): workaround webpack bug replacing this with just
|
||||
// `__webpack_require__.g,` which causes script error
|
||||
global: globalThis,
|
||||
setImmediate,
|
||||
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);
|
||||
101
lib/sandboxed_renderer/preload.ts
Normal file
101
lib/sandboxed_renderer/preload.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages';
|
||||
import { ipcRendererInternal } from '@electron/internal/renderer/ipc-renderer-internal';
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
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 = () => {
|
||||
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);
|
||||
ipcRendererInternal.send(IPC_MESSAGES.BROWSER_PRELOAD_ERROR, filePath, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -139,4 +139,4 @@ refactor_unfilter_unresponsive_events.patch
|
||||
build_disable_thin_lto_mac.patch
|
||||
build_add_public_config_simdutf_config.patch
|
||||
revert_code_health_clean_up_stale_macwebcontentsocclusion.patch
|
||||
check_for_unit_to_activate_before_notifying_about_success.patch
|
||||
feat_add_signals_when_embedder_cleanup_callbacks_run_for.patch
|
||||
|
||||
@@ -1,497 +0,0 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Maksim Sisov <msisov@igalia.com>
|
||||
Date: Tue, 7 Jan 2025 23:46:56 -0800
|
||||
Subject: Check for unit to activate before notifying about success
|
||||
|
||||
Portal's globalShortcuts interface relies on the unit name to
|
||||
properly assign a client for the bound commands. However, in
|
||||
some scenarious, there is a race between the service to be
|
||||
created, changed its name and activated. As a result, shortcuts
|
||||
might be bound before the name is changed. As a result, shortcuts
|
||||
might not correctly work and the client will not receive any
|
||||
signals.
|
||||
|
||||
This is mostly not an issue for Chromium as it creates the
|
||||
global shortcuts portal linux object way earlier than it gets
|
||||
commands from the command service. But downstream project, which
|
||||
try to bind shortcuts at the same time as that instance is created,
|
||||
may experience this issue. As a result, they might not have
|
||||
shortcuts working correctly after system reboot or app restart as
|
||||
there is a race between those operations.
|
||||
|
||||
Bug: None
|
||||
Change-Id: I8346d65e051d9587850c76ca0b8807669c161667
|
||||
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6110782
|
||||
Reviewed-by: Thomas Anderson <thomasanderson@chromium.org>
|
||||
Commit-Queue: Maksim Sisov <msisov@igalia.com>
|
||||
Cr-Commit-Position: refs/heads/main@{#1403434}
|
||||
|
||||
diff --git a/components/dbus/xdg/systemd.cc b/components/dbus/xdg/systemd.cc
|
||||
index 362a16447bf578923cb8a84674c277ae6c98228f..3cd9a55d540c07a4c53f5a62bec5cbea37c11838 100644
|
||||
--- a/components/dbus/xdg/systemd.cc
|
||||
+++ b/components/dbus/xdg/systemd.cc
|
||||
@@ -4,9 +4,12 @@
|
||||
|
||||
#include "components/dbus/xdg/systemd.h"
|
||||
|
||||
+#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "base/environment.h"
|
||||
+#include "base/functional/bind.h"
|
||||
+#include "base/functional/callback_helpers.h"
|
||||
#include "base/memory/scoped_refptr.h"
|
||||
#include "base/no_destructor.h"
|
||||
#include "base/sequence_checker.h"
|
||||
@@ -17,7 +20,9 @@
|
||||
#include "components/dbus/utils/name_has_owner.h"
|
||||
#include "dbus/bus.h"
|
||||
#include "dbus/message.h"
|
||||
+#include "dbus/object_path.h"
|
||||
#include "dbus/object_proxy.h"
|
||||
+#include "dbus/property.h"
|
||||
#include "third_party/abseil-cpp/absl/types/variant.h"
|
||||
|
||||
namespace dbus_xdg {
|
||||
@@ -37,6 +42,10 @@ constexpr char kServiceNameSystemd[] = "org.freedesktop.systemd1";
|
||||
constexpr char kObjectPathSystemd[] = "/org/freedesktop/systemd1";
|
||||
constexpr char kInterfaceSystemdManager[] = "org.freedesktop.systemd1.Manager";
|
||||
constexpr char kMethodStartTransientUnit[] = "StartTransientUnit";
|
||||
+constexpr char kMethodGetUnit[] = "GetUnit";
|
||||
+
|
||||
+constexpr char kInterfaceSystemdUnit[] = "org.freedesktop.systemd1.Unit";
|
||||
+constexpr char kActiveStateProp[] = "ActiveState";
|
||||
|
||||
constexpr char kUnitNameFormat[] = "app-$1$2-$3.scope";
|
||||
|
||||
@@ -67,6 +76,81 @@ const char* GetAppNameSuffix(const std::string& channel) {
|
||||
return "";
|
||||
}
|
||||
|
||||
+// Declare this helper for SystemdUnitActiveStateWatcher to be used.
|
||||
+void SetStateAndRunCallbacks(SystemdUnitStatus result);
|
||||
+
|
||||
+// Watches the object to become active and fires callbacks via
|
||||
+// SetStateAndRunCallbacks. The callbacks are fired whenever a response with the
|
||||
+// state being "active" or "failed" (or similar) comes.
|
||||
+//
|
||||
+// PS firing callbacks results in destroying this object. So any references
|
||||
+// to this become invalid.
|
||||
+class SystemdUnitActiveStateWatcher : public dbus::PropertySet {
|
||||
+ public:
|
||||
+ SystemdUnitActiveStateWatcher(scoped_refptr<dbus::Bus> bus,
|
||||
+ dbus::ObjectProxy* object_proxy)
|
||||
+ : dbus::PropertySet(object_proxy,
|
||||
+ kInterfaceSystemdUnit,
|
||||
+ base::BindRepeating(
|
||||
+ &SystemdUnitActiveStateWatcher::OnPropertyChanged,
|
||||
+ base::Unretained(this))),
|
||||
+ bus_(bus) {
|
||||
+ RegisterProperty(kActiveStateProp, &active_state_);
|
||||
+ ConnectSignals();
|
||||
+ GetAll();
|
||||
+ }
|
||||
+
|
||||
+ ~SystemdUnitActiveStateWatcher() override {
|
||||
+ bus_->RemoveObjectProxy(kServiceNameSystemd, object_proxy()->object_path(),
|
||||
+ base::DoNothing());
|
||||
+ }
|
||||
+
|
||||
+ private:
|
||||
+ void OnPropertyChanged(const std::string& property_name) {
|
||||
+ DCHECK(active_state_.is_valid());
|
||||
+ const std::string state_value = active_state_.value();
|
||||
+ if (callbacks_called_ || state_value == "activating" ||
|
||||
+ state_value == "reloading") {
|
||||
+ // Ignore if callbacks have already been fired or continue waiting until
|
||||
+ // the state changes to something else.
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ // There are other states as failed, inactive, and deactivating. Treat all
|
||||
+ // of them as failed.
|
||||
+ callbacks_called_ = true;
|
||||
+ SetStateAndRunCallbacks(state_value == "active"
|
||||
+ ? SystemdUnitStatus::kUnitStarted
|
||||
+ : SystemdUnitStatus::kFailedToStart);
|
||||
+ MaybeDeleteSelf();
|
||||
+ }
|
||||
+
|
||||
+ void OnGetAll(dbus::Response* response) override {
|
||||
+ dbus::PropertySet::OnGetAll(response);
|
||||
+ keep_alive_ = false;
|
||||
+ MaybeDeleteSelf();
|
||||
+ }
|
||||
+
|
||||
+ void MaybeDeleteSelf() {
|
||||
+ if (!keep_alive_ && callbacks_called_) {
|
||||
+ delete this;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ // Registered property that this listens updates to.
|
||||
+ dbus::Property<std::string> active_state_;
|
||||
+
|
||||
+ // Indicates whether callbacks for the unit's state have been called.
|
||||
+ bool callbacks_called_ = false;
|
||||
+
|
||||
+ // Control variable that helps to defer the destruction of |this| as deleting
|
||||
+ // self when the state changes to active during |OnGetAll| will result in a
|
||||
+ // segfault.
|
||||
+ bool keep_alive_ = true;
|
||||
+
|
||||
+ scoped_refptr<dbus::Bus> bus_;
|
||||
+};
|
||||
+
|
||||
// Global state for cached result or pending callbacks.
|
||||
StatusOrCallbacks& GetUnitNameState() {
|
||||
static base::NoDestructor<StatusOrCallbacks> state(
|
||||
@@ -83,10 +167,52 @@ void SetStateAndRunCallbacks(SystemdUnitStatus result) {
|
||||
}
|
||||
}
|
||||
|
||||
-void OnStartTransientUnitResponse(dbus::Response* response) {
|
||||
+void OnGetPathResponse(scoped_refptr<dbus::Bus> bus, dbus::Response* response) {
|
||||
+ dbus::MessageReader reader(response);
|
||||
+ dbus::ObjectPath obj_path;
|
||||
+ if (!response || !reader.PopObjectPath(&obj_path) || !obj_path.IsValid()) {
|
||||
+ // We didn't get a valid response. Treat this as failed service.
|
||||
+ SetStateAndRunCallbacks(SystemdUnitStatus::kFailedToStart);
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ dbus::ObjectProxy* unit_proxy =
|
||||
+ bus->GetObjectProxy(kServiceNameSystemd, obj_path);
|
||||
+ // Create the active state property watcher. It will destroy itself once
|
||||
+ // it gets notified about the state change.
|
||||
+ std::unique_ptr<SystemdUnitActiveStateWatcher> active_state_watcher =
|
||||
+ std::make_unique<SystemdUnitActiveStateWatcher>(bus, unit_proxy);
|
||||
+ active_state_watcher.release();
|
||||
+}
|
||||
+
|
||||
+void WaitUnitActivateAndRunCallbacks(scoped_refptr<dbus::Bus> bus,
|
||||
+ std::string unit_name) {
|
||||
+ // Get the path of the unit, which looks similar to
|
||||
+ // /org/freedesktop/systemd1/unit/app_2dorg_2echromium_2eChromium_2d3182191_2escope
|
||||
+ // and then wait for it activation.
|
||||
+ dbus::ObjectProxy* systemd = bus->GetObjectProxy(
|
||||
+ kServiceNameSystemd, dbus::ObjectPath(kObjectPathSystemd));
|
||||
+
|
||||
+ dbus::MethodCall method_call(kInterfaceSystemdManager, kMethodGetUnit);
|
||||
+ dbus::MessageWriter writer(&method_call);
|
||||
+ writer.AppendString(unit_name);
|
||||
+
|
||||
+ systemd->CallMethod(&method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
|
||||
+ base::BindOnce(&OnGetPathResponse, std::move(bus)));
|
||||
+}
|
||||
+
|
||||
+void OnStartTransientUnitResponse(scoped_refptr<dbus::Bus> bus,
|
||||
+ std::string unit_name,
|
||||
+ dbus::Response* response) {
|
||||
SystemdUnitStatus result = response ? SystemdUnitStatus::kUnitStarted
|
||||
: SystemdUnitStatus::kFailedToStart;
|
||||
- SetStateAndRunCallbacks(result);
|
||||
+ // If the start of the unit failed, immediately notify the client. Otherwise,
|
||||
+ // wait for its activation.
|
||||
+ if (result == SystemdUnitStatus::kFailedToStart) {
|
||||
+ SetStateAndRunCallbacks(result);
|
||||
+ } else {
|
||||
+ WaitUnitActivateAndRunCallbacks(std::move(bus), unit_name);
|
||||
+ }
|
||||
}
|
||||
|
||||
void OnNameHasOwnerResponse(scoped_refptr<dbus::Bus> bus,
|
||||
@@ -128,8 +254,9 @@ void OnNameHasOwnerResponse(scoped_refptr<dbus::Bus> bus,
|
||||
properties.Write(&writer);
|
||||
// No auxiliary units.
|
||||
Dict<VarDict>().Write(&writer);
|
||||
- systemd->CallMethod(&method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
|
||||
- base::BindOnce(&OnStartTransientUnitResponse));
|
||||
+ systemd->CallMethod(
|
||||
+ &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
|
||||
+ base::BindOnce(&OnStartTransientUnitResponse, std::move(bus), unit_name));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
diff --git a/components/dbus/xdg/systemd_unittest.cc b/components/dbus/xdg/systemd_unittest.cc
|
||||
index 2e3baecabc4b479000c78d4f6bd30cb1f6e61d2e..67278d7033664d52fbbda02749a2aaa43352f402 100644
|
||||
--- a/components/dbus/xdg/systemd_unittest.cc
|
||||
+++ b/components/dbus/xdg/systemd_unittest.cc
|
||||
@@ -16,7 +16,9 @@
|
||||
#include "dbus/message.h"
|
||||
#include "dbus/mock_bus.h"
|
||||
#include "dbus/mock_object_proxy.h"
|
||||
+#include "dbus/object_path.h"
|
||||
#include "dbus/object_proxy.h"
|
||||
+#include "dbus/property.h"
|
||||
#include "testing/gmock/include/gmock/gmock.h"
|
||||
#include "testing/gtest/include/gtest/gtest.h"
|
||||
|
||||
@@ -32,6 +34,27 @@ constexpr char kServiceNameSystemd[] = "org.freedesktop.systemd1";
|
||||
constexpr char kObjectPathSystemd[] = "/org/freedesktop/systemd1";
|
||||
constexpr char kInterfaceSystemdManager[] = "org.freedesktop.systemd1.Manager";
|
||||
constexpr char kMethodStartTransientUnit[] = "StartTransientUnit";
|
||||
+constexpr char kMethodGetUnit[] = "GetUnit";
|
||||
+
|
||||
+constexpr char kFakeUnitPath[] = "/fake/unit/path";
|
||||
+constexpr char kActiveState[] = "ActiveState";
|
||||
+constexpr char kStateActive[] = "active";
|
||||
+constexpr char kStateInactive[] = "inactive";
|
||||
+
|
||||
+std::unique_ptr<dbus::Response> CreateActiveStateGetAllResponse(
|
||||
+ const std::string& state) {
|
||||
+ auto response = dbus::Response::CreateEmpty();
|
||||
+ dbus::MessageWriter writer(response.get());
|
||||
+ dbus::MessageWriter array_writer(nullptr);
|
||||
+ dbus::MessageWriter dict_entry_writer(nullptr);
|
||||
+ writer.OpenArray("{sv}", &array_writer);
|
||||
+ array_writer.OpenDictEntry(&dict_entry_writer);
|
||||
+ dict_entry_writer.AppendString(kActiveState);
|
||||
+ dict_entry_writer.AppendVariantOfString(state);
|
||||
+ array_writer.CloseContainer(&dict_entry_writer);
|
||||
+ writer.CloseContainer(&array_writer);
|
||||
+ return response;
|
||||
+}
|
||||
|
||||
class SetSystemdScopeUnitNameForXdgPortalTest : public ::testing::Test {
|
||||
public:
|
||||
@@ -124,17 +147,48 @@ TEST_F(SetSystemdScopeUnitNameForXdgPortalTest, StartTransientUnitSuccess) {
|
||||
|
||||
EXPECT_CALL(*bus, GetObjectProxy(kServiceNameSystemd,
|
||||
dbus::ObjectPath(kObjectPathSystemd)))
|
||||
- .WillOnce(Return(mock_systemd_proxy.get()));
|
||||
+ .Times(2)
|
||||
+ .WillRepeatedly(Return(mock_systemd_proxy.get()));
|
||||
+
|
||||
+ auto mock_dbus_unit_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
|
||||
+ bus.get(), kServiceNameSystemd, dbus::ObjectPath(kFakeUnitPath));
|
||||
+ EXPECT_CALL(*bus, GetObjectProxy(kServiceNameSystemd,
|
||||
+ dbus::ObjectPath(kFakeUnitPath)))
|
||||
+ .WillOnce(Return(mock_dbus_unit_proxy.get()));
|
||||
|
||||
EXPECT_CALL(*mock_systemd_proxy, DoCallMethod(_, _, _))
|
||||
.WillOnce(Invoke([](dbus::MethodCall* method_call, int timeout_ms,
|
||||
dbus::ObjectProxy::ResponseCallback* callback) {
|
||||
+ // Expect kMethodStartTransientUnit first.
|
||||
EXPECT_EQ(method_call->GetInterface(), kInterfaceSystemdManager);
|
||||
EXPECT_EQ(method_call->GetMember(), kMethodStartTransientUnit);
|
||||
|
||||
// Simulate a successful response
|
||||
auto response = dbus::Response::CreateEmpty();
|
||||
std::move(*callback).Run(response.get());
|
||||
+ }))
|
||||
+ .WillOnce(Invoke([obj_path = kFakeUnitPath](
|
||||
+ dbus::MethodCall* method_call, int timeout_ms,
|
||||
+ dbus::ObjectProxy::ResponseCallback* callback) {
|
||||
+ // Then expect kMethodGetUnit. A valid path must be provided.
|
||||
+ EXPECT_EQ(method_call->GetInterface(), kInterfaceSystemdManager);
|
||||
+ EXPECT_EQ(method_call->GetMember(), kMethodGetUnit);
|
||||
+
|
||||
+ // Simulate a successful response and provide a fake path to the object.
|
||||
+ auto response = dbus::Response::CreateEmpty();
|
||||
+ dbus::MessageWriter writer(response.get());
|
||||
+ writer.AppendObjectPath(dbus::ObjectPath(obj_path));
|
||||
+ std::move(*callback).Run(response.get());
|
||||
+ }));
|
||||
+
|
||||
+ EXPECT_CALL(*mock_dbus_unit_proxy, DoCallMethod(_, _, _))
|
||||
+ .WillOnce(Invoke([](dbus::MethodCall* method_call, int timeout_ms,
|
||||
+ dbus::ObjectProxy::ResponseCallback* callback) {
|
||||
+ EXPECT_EQ(method_call->GetInterface(), dbus::kPropertiesInterface);
|
||||
+ EXPECT_EQ(method_call->GetMember(), dbus::kPropertiesGetAll);
|
||||
+ // Simulate a successful response with "active" state.
|
||||
+ auto response = CreateActiveStateGetAllResponse(kStateActive);
|
||||
+ std::move(*callback).Run(response.get());
|
||||
}));
|
||||
|
||||
std::optional<SystemdUnitStatus> status;
|
||||
@@ -189,6 +243,142 @@ TEST_F(SetSystemdScopeUnitNameForXdgPortalTest, StartTransientUnitFailure) {
|
||||
EXPECT_EQ(status, SystemdUnitStatus::kFailedToStart);
|
||||
}
|
||||
|
||||
+TEST_F(SetSystemdScopeUnitNameForXdgPortalTest,
|
||||
+ StartTransientUnitInvalidUnitPath) {
|
||||
+ scoped_refptr<dbus::MockBus> bus =
|
||||
+ base::MakeRefCounted<dbus::MockBus>(dbus::Bus::Options());
|
||||
+
|
||||
+ auto mock_dbus_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
|
||||
+ bus.get(), DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS));
|
||||
+
|
||||
+ EXPECT_CALL(
|
||||
+ *bus, GetObjectProxy(DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS)))
|
||||
+ .WillRepeatedly(Return(mock_dbus_proxy.get()));
|
||||
+
|
||||
+ EXPECT_CALL(*mock_dbus_proxy, DoCallMethod(_, _, _))
|
||||
+ .WillOnce(Invoke([](dbus::MethodCall* method_call, int timeout_ms,
|
||||
+ dbus::ObjectProxy::ResponseCallback* callback) {
|
||||
+ auto response = dbus::Response::CreateEmpty();
|
||||
+ dbus::MessageWriter writer(response.get());
|
||||
+ writer.AppendBool(true);
|
||||
+ std::move(*callback).Run(response.get());
|
||||
+ }));
|
||||
+
|
||||
+ auto mock_systemd_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
|
||||
+ bus.get(), kServiceNameSystemd, dbus::ObjectPath(kObjectPathSystemd));
|
||||
+
|
||||
+ EXPECT_CALL(*bus, GetObjectProxy(kServiceNameSystemd,
|
||||
+ dbus::ObjectPath(kObjectPathSystemd)))
|
||||
+ .Times(2)
|
||||
+ .WillRepeatedly(Return(mock_systemd_proxy.get()));
|
||||
+
|
||||
+ EXPECT_CALL(*mock_systemd_proxy, DoCallMethod(_, _, _))
|
||||
+ .WillOnce(Invoke([](dbus::MethodCall* method_call, int timeout_ms,
|
||||
+ dbus::ObjectProxy::ResponseCallback* callback) {
|
||||
+ EXPECT_EQ(method_call->GetInterface(), kInterfaceSystemdManager);
|
||||
+ EXPECT_EQ(method_call->GetMember(), kMethodStartTransientUnit);
|
||||
+
|
||||
+ // Simulate a successful response
|
||||
+ auto response = dbus::Response::CreateEmpty();
|
||||
+ std::move(*callback).Run(response.get());
|
||||
+ }))
|
||||
+ .WillOnce(Invoke([](dbus::MethodCall* method_call, int timeout_ms,
|
||||
+ dbus::ObjectProxy::ResponseCallback* callback) {
|
||||
+ EXPECT_EQ(method_call->GetInterface(), kInterfaceSystemdManager);
|
||||
+ EXPECT_EQ(method_call->GetMember(), kMethodGetUnit);
|
||||
+
|
||||
+ // Simulate a failure response.
|
||||
+ std::move(*callback).Run(nullptr);
|
||||
+ }));
|
||||
+
|
||||
+ std::optional<SystemdUnitStatus> status;
|
||||
+
|
||||
+ SetSystemdScopeUnitNameForXdgPortal(
|
||||
+ bus.get(), base::BindLambdaForTesting(
|
||||
+ [&](SystemdUnitStatus result) { status = result; }));
|
||||
+
|
||||
+ EXPECT_EQ(status, SystemdUnitStatus::kFailedToStart);
|
||||
+}
|
||||
+
|
||||
+TEST_F(SetSystemdScopeUnitNameForXdgPortalTest,
|
||||
+ StartTransientUnitFailToActivate) {
|
||||
+ scoped_refptr<dbus::MockBus> bus =
|
||||
+ base::MakeRefCounted<dbus::MockBus>(dbus::Bus::Options());
|
||||
+
|
||||
+ auto mock_dbus_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
|
||||
+ bus.get(), DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS));
|
||||
+
|
||||
+ EXPECT_CALL(
|
||||
+ *bus, GetObjectProxy(DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS)))
|
||||
+ .WillRepeatedly(Return(mock_dbus_proxy.get()));
|
||||
+
|
||||
+ EXPECT_CALL(*mock_dbus_proxy, DoCallMethod(_, _, _))
|
||||
+ .WillOnce(Invoke([](dbus::MethodCall* method_call, int timeout_ms,
|
||||
+ dbus::ObjectProxy::ResponseCallback* callback) {
|
||||
+ auto response = dbus::Response::CreateEmpty();
|
||||
+ dbus::MessageWriter writer(response.get());
|
||||
+ writer.AppendBool(true);
|
||||
+ std::move(*callback).Run(response.get());
|
||||
+ }));
|
||||
+
|
||||
+ auto mock_systemd_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
|
||||
+ bus.get(), kServiceNameSystemd, dbus::ObjectPath(kObjectPathSystemd));
|
||||
+
|
||||
+ EXPECT_CALL(*bus, GetObjectProxy(kServiceNameSystemd,
|
||||
+ dbus::ObjectPath(kObjectPathSystemd)))
|
||||
+ .Times(2)
|
||||
+ .WillRepeatedly(Return(mock_systemd_proxy.get()));
|
||||
+
|
||||
+ auto mock_dbus_unit_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
|
||||
+ bus.get(), kServiceNameSystemd, dbus::ObjectPath(kFakeUnitPath));
|
||||
+ EXPECT_CALL(*bus, GetObjectProxy(kServiceNameSystemd,
|
||||
+ dbus::ObjectPath(kFakeUnitPath)))
|
||||
+ .WillOnce(Return(mock_dbus_unit_proxy.get()));
|
||||
+
|
||||
+ EXPECT_CALL(*mock_systemd_proxy, DoCallMethod(_, _, _))
|
||||
+ .WillOnce(Invoke([](dbus::MethodCall* method_call, int timeout_ms,
|
||||
+ dbus::ObjectProxy::ResponseCallback* callback) {
|
||||
+ EXPECT_EQ(method_call->GetInterface(), kInterfaceSystemdManager);
|
||||
+ EXPECT_EQ(method_call->GetMember(), kMethodStartTransientUnit);
|
||||
+
|
||||
+ // Simulate a successful response
|
||||
+ auto response = dbus::Response::CreateEmpty();
|
||||
+ std::move(*callback).Run(response.get());
|
||||
+ }))
|
||||
+ .WillOnce(Invoke([obj_path = kFakeUnitPath](
|
||||
+ dbus::MethodCall* method_call, int timeout_ms,
|
||||
+ dbus::ObjectProxy::ResponseCallback* callback) {
|
||||
+ EXPECT_EQ(method_call->GetInterface(), kInterfaceSystemdManager);
|
||||
+ EXPECT_EQ(method_call->GetMember(), kMethodGetUnit);
|
||||
+
|
||||
+ // Simulate a successful response
|
||||
+ auto response = dbus::Response::CreateEmpty();
|
||||
+ dbus::MessageWriter writer(response.get());
|
||||
+ writer.AppendObjectPath(dbus::ObjectPath(obj_path));
|
||||
+ std::move(*callback).Run(response.get());
|
||||
+ }));
|
||||
+
|
||||
+ EXPECT_CALL(*mock_dbus_unit_proxy, DoCallMethod(_, _, _))
|
||||
+ .WillOnce(Invoke([](dbus::MethodCall* method_call, int timeout_ms,
|
||||
+ dbus::ObjectProxy::ResponseCallback* callback) {
|
||||
+ // Then expect kMethodGetUnit. A valid path must be provided.
|
||||
+ EXPECT_EQ(method_call->GetInterface(), dbus::kPropertiesInterface);
|
||||
+ EXPECT_EQ(method_call->GetMember(), dbus::kPropertiesGetAll);
|
||||
+
|
||||
+ // Simulate a successful response, but with inactive state.
|
||||
+ auto response = CreateActiveStateGetAllResponse(kStateInactive);
|
||||
+ std::move(*callback).Run(response.get());
|
||||
+ }));
|
||||
+
|
||||
+ std::optional<SystemdUnitStatus> status;
|
||||
+
|
||||
+ SetSystemdScopeUnitNameForXdgPortal(
|
||||
+ bus.get(), base::BindLambdaForTesting(
|
||||
+ [&](SystemdUnitStatus result) { status = result; }));
|
||||
+
|
||||
+ EXPECT_EQ(status, SystemdUnitStatus::kFailedToStart);
|
||||
+}
|
||||
+
|
||||
TEST_F(SetSystemdScopeUnitNameForXdgPortalTest, UnitNameConstruction) {
|
||||
scoped_refptr<dbus::MockBus> bus =
|
||||
base::MakeRefCounted<dbus::MockBus>(dbus::Bus::Options());
|
||||
@@ -220,7 +410,14 @@ TEST_F(SetSystemdScopeUnitNameForXdgPortalTest, UnitNameConstruction) {
|
||||
|
||||
EXPECT_CALL(*bus, GetObjectProxy(kServiceNameSystemd,
|
||||
dbus::ObjectPath(kObjectPathSystemd)))
|
||||
- .WillOnce(Return(mock_systemd_proxy.get()));
|
||||
+ .Times(2)
|
||||
+ .WillRepeatedly(Return(mock_systemd_proxy.get()));
|
||||
+
|
||||
+ auto mock_dbus_unit_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
|
||||
+ bus.get(), kServiceNameSystemd, dbus::ObjectPath(kFakeUnitPath));
|
||||
+ EXPECT_CALL(*bus, GetObjectProxy(kServiceNameSystemd,
|
||||
+ dbus::ObjectPath(kFakeUnitPath)))
|
||||
+ .WillOnce(Return(mock_dbus_unit_proxy.get()));
|
||||
|
||||
EXPECT_CALL(*mock_systemd_proxy, DoCallMethod(_, _, _))
|
||||
.WillOnce(Invoke([&](dbus::MethodCall* method_call, int timeout_ms,
|
||||
@@ -256,6 +453,30 @@ TEST_F(SetSystemdScopeUnitNameForXdgPortalTest, UnitNameConstruction) {
|
||||
|
||||
auto response = dbus::Response::CreateEmpty();
|
||||
std::move(*callback).Run(response.get());
|
||||
+ }))
|
||||
+ .WillOnce(Invoke([obj_path = kFakeUnitPath](
|
||||
+ dbus::MethodCall* method_call, int timeout_ms,
|
||||
+ dbus::ObjectProxy::ResponseCallback* callback) {
|
||||
+ EXPECT_EQ(method_call->GetInterface(), kInterfaceSystemdManager);
|
||||
+ EXPECT_EQ(method_call->GetMember(), kMethodGetUnit);
|
||||
+
|
||||
+ // Simulate a successful response
|
||||
+ auto response = dbus::Response::CreateEmpty();
|
||||
+ dbus::MessageWriter writer(response.get());
|
||||
+ writer.AppendObjectPath(dbus::ObjectPath(obj_path));
|
||||
+ std::move(*callback).Run(response.get());
|
||||
+ }));
|
||||
+
|
||||
+ EXPECT_CALL(*mock_dbus_unit_proxy, DoCallMethod(_, _, _))
|
||||
+ .WillOnce(Invoke([](dbus::MethodCall* method_call, int timeout_ms,
|
||||
+ dbus::ObjectProxy::ResponseCallback* callback) {
|
||||
+ // Then expect kMethodGetUnit. A valid path must be provided.
|
||||
+ EXPECT_EQ(method_call->GetInterface(), dbus::kPropertiesInterface);
|
||||
+ EXPECT_EQ(method_call->GetMember(), dbus::kPropertiesGetAll);
|
||||
+
|
||||
+ // Simulate a successful response
|
||||
+ auto response = CreateActiveStateGetAllResponse(kStateActive);
|
||||
+ std::move(*callback).Run(response.get());
|
||||
}));
|
||||
|
||||
std::optional<SystemdUnitStatus> status;
|
||||
@@ -0,0 +1,168 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: deepak1556 <hop2deep@gmail.com>
|
||||
Date: Wed, 29 Jan 2025 17:01:03 +0900
|
||||
Subject: feat: add signals when embedder cleanup callbacks run for
|
||||
gin::wrappable
|
||||
|
||||
Current setup of finalization callbacks does not work well with
|
||||
gin_helper::CleanedUpAtExit for wrappables specifically on environment
|
||||
shutdown leading to UAF in the second pass.
|
||||
|
||||
Details at https://github.com/microsoft/vscode/issues/192119#issuecomment-2375851531
|
||||
|
||||
The signals exposed in this patch does the following 2 things,
|
||||
|
||||
1) Fix weak state of the wrapped object when the finializer callbacks
|
||||
have not yet been processed
|
||||
2) Avoid calling into the second pass when the embedder has already
|
||||
destroyed the wrapped object via CleanedUpAtExit.
|
||||
|
||||
This patch is more of a bandaid fix to improve the lifetime
|
||||
management with existing finalizer callbacks. We should be able to
|
||||
remove this patch once gin::Wrappable can be managed by V8 Oilpan
|
||||
|
||||
Refs https://issues.chromium.org/issues/40210365 which is blocked
|
||||
on https://issues.chromium.org/issues/42203693
|
||||
|
||||
diff --git a/gin/isolate_holder.cc b/gin/isolate_holder.cc
|
||||
index e5ee2c6b3cb787ff9f8272d4344a1e18c44971e2..22469cf0ab1025eefcf94e2cd351087e52182130 100644
|
||||
--- a/gin/isolate_holder.cc
|
||||
+++ b/gin/isolate_holder.cc
|
||||
@@ -34,6 +34,8 @@ v8::ArrayBuffer::Allocator* g_array_buffer_allocator = nullptr;
|
||||
const intptr_t* g_reference_table = nullptr;
|
||||
v8::FatalErrorCallback g_fatal_error_callback = nullptr;
|
||||
v8::OOMErrorCallback g_oom_error_callback = nullptr;
|
||||
+bool g_initialized_microtasks_runner = false;
|
||||
+bool g_destroyed_microtasks_runner = false;
|
||||
|
||||
std::unique_ptr<v8::Isolate::CreateParams> getModifiedIsolateParams(
|
||||
std::unique_ptr<v8::Isolate::CreateParams> params,
|
||||
@@ -194,10 +196,26 @@ IsolateHolder::getDefaultIsolateParams() {
|
||||
return params;
|
||||
}
|
||||
|
||||
+// static
|
||||
+bool IsolateHolder::DestroyedMicrotasksRunner() {
|
||||
+ return g_initialized_microtasks_runner &&
|
||||
+ g_destroyed_microtasks_runner;
|
||||
+}
|
||||
+
|
||||
void IsolateHolder::EnableIdleTasks(
|
||||
std::unique_ptr<V8IdleTaskRunner> idle_task_runner) {
|
||||
DCHECK(isolate_data_.get());
|
||||
isolate_data_->EnableIdleTasks(std::move(idle_task_runner));
|
||||
}
|
||||
|
||||
+void IsolateHolder::WillCreateMicrotasksRunner() {
|
||||
+ DCHECK(!g_initialized_microtasks_runner);
|
||||
+ g_initialized_microtasks_runner = true;
|
||||
+}
|
||||
+
|
||||
+void IsolateHolder::WillDestroyMicrotasksRunner() {
|
||||
+ DCHECK(g_initialized_microtasks_runner);
|
||||
+ g_destroyed_microtasks_runner = true;
|
||||
+}
|
||||
+
|
||||
} // namespace gin
|
||||
diff --git a/gin/public/isolate_holder.h b/gin/public/isolate_holder.h
|
||||
index c22b0a7f9af621573e888a518ccdc22293ce07ef..d3e5ced425df54f42534cec5cc0c5bbfb9d79c6c 100644
|
||||
--- a/gin/public/isolate_holder.h
|
||||
+++ b/gin/public/isolate_holder.h
|
||||
@@ -130,6 +130,8 @@ class GIN_EXPORT IsolateHolder {
|
||||
// Should only be called after v8::IsolateHolder::Initialize() is invoked.
|
||||
static std::unique_ptr<v8::Isolate::CreateParams> getDefaultIsolateParams();
|
||||
|
||||
+ static bool DestroyedMicrotasksRunner();
|
||||
+
|
||||
v8::Isolate* isolate() { return isolate_; }
|
||||
|
||||
// This method returns if v8::Locker is needed to access isolate.
|
||||
@@ -143,6 +145,9 @@ class GIN_EXPORT IsolateHolder {
|
||||
|
||||
void EnableIdleTasks(std::unique_ptr<V8IdleTaskRunner> idle_task_runner);
|
||||
|
||||
+ void WillCreateMicrotasksRunner();
|
||||
+ void WillDestroyMicrotasksRunner();
|
||||
+
|
||||
// This method returns V8IsolateMemoryDumpProvider of this isolate, used for
|
||||
// testing.
|
||||
V8IsolateMemoryDumpProvider* isolate_memory_dump_provider_for_testing()
|
||||
diff --git a/gin/wrappable.cc b/gin/wrappable.cc
|
||||
index 402355cb836cea14e9ee725a142a4bad44fd5bed..7e7f028dcfb87c7b80adebabac19ced8791f642e 100644
|
||||
--- a/gin/wrappable.cc
|
||||
+++ b/gin/wrappable.cc
|
||||
@@ -13,6 +13,9 @@ namespace gin {
|
||||
WrappableBase::WrappableBase() = default;
|
||||
|
||||
WrappableBase::~WrappableBase() {
|
||||
+ if (!wrapper_.IsEmpty()) {
|
||||
+ wrapper_.ClearWeak();
|
||||
+ }
|
||||
wrapper_.Reset();
|
||||
}
|
||||
|
||||
@@ -28,15 +31,24 @@ const char* WrappableBase::GetTypeName() {
|
||||
void WrappableBase::FirstWeakCallback(
|
||||
const v8::WeakCallbackInfo<WrappableBase>& data) {
|
||||
WrappableBase* wrappable = data.GetParameter();
|
||||
- wrappable->dead_ = true;
|
||||
- wrappable->wrapper_.Reset();
|
||||
- data.SetSecondPassCallback(SecondWeakCallback);
|
||||
+ WrappableBase* wrappable_from_field =
|
||||
+ static_cast<WrappableBase*>(data.GetInternalField(1));
|
||||
+ if (wrappable && wrappable == wrappable_from_field) {
|
||||
+ wrappable->dead_ = true;
|
||||
+ wrappable->wrapper_.Reset();
|
||||
+ data.SetSecondPassCallback(SecondWeakCallback);
|
||||
+ }
|
||||
}
|
||||
|
||||
void WrappableBase::SecondWeakCallback(
|
||||
const v8::WeakCallbackInfo<WrappableBase>& data) {
|
||||
+ if (IsolateHolder::DestroyedMicrotasksRunner()) {
|
||||
+ return;
|
||||
+ }
|
||||
WrappableBase* wrappable = data.GetParameter();
|
||||
- delete wrappable;
|
||||
+ if (wrappable) {
|
||||
+ delete wrappable;
|
||||
+ }
|
||||
}
|
||||
|
||||
v8::MaybeLocal<v8::Object> WrappableBase::GetWrapperImpl(v8::Isolate* isolate,
|
||||
@@ -71,10 +83,16 @@ v8::MaybeLocal<v8::Object> WrappableBase::GetWrapperImpl(v8::Isolate* isolate,
|
||||
void* values[] = {info, this};
|
||||
wrapper->SetAlignedPointerInInternalFields(2, indices, values);
|
||||
wrapper_.Reset(isolate, wrapper);
|
||||
- wrapper_.SetWeak(this, FirstWeakCallback, v8::WeakCallbackType::kParameter);
|
||||
+ wrapper_.SetWeak(this, FirstWeakCallback, v8::WeakCallbackType::kInternalFields);
|
||||
return v8::MaybeLocal<v8::Object>(wrapper);
|
||||
}
|
||||
|
||||
+void WrappableBase::ClearWeak() {
|
||||
+ if (!wrapper_.IsEmpty()) {
|
||||
+ wrapper_.ClearWeak();
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
namespace internal {
|
||||
|
||||
void* FromV8Impl(v8::Isolate* isolate, v8::Local<v8::Value> val,
|
||||
diff --git a/gin/wrappable.h b/gin/wrappable.h
|
||||
index 4e7115685a5bf6997e78edcc1851e28bd00b1aa2..ca51fe33605e855438e88969e3d3cc734ef4523e 100644
|
||||
--- a/gin/wrappable.h
|
||||
+++ b/gin/wrappable.h
|
||||
@@ -80,6 +80,13 @@ class GIN_EXPORT WrappableBase {
|
||||
v8::MaybeLocal<v8::Object> GetWrapperImpl(v8::Isolate* isolate,
|
||||
WrapperInfo* wrapper_info);
|
||||
|
||||
+ // Make this wrappable strong again. This is useful when the wrappable is
|
||||
+ // destroyed outside the finalizer callbacks and we want to avoid scheduling
|
||||
+ // the weak callbacks if they haven't been scheduled yet.
|
||||
+ // NOTE!!! this does not prevent finalization callbacks from running if they
|
||||
+ // have already been processed.
|
||||
+ void ClearWeak();
|
||||
+
|
||||
private:
|
||||
static void FirstWeakCallback(
|
||||
const v8::WeakCallbackInfo<WrappableBase>& data);
|
||||
@@ -14,10 +14,25 @@ It also:
|
||||
This may be partially upstreamed to Chromium in the future.
|
||||
|
||||
diff --git a/ui/gtk/select_file_dialog_linux_gtk.cc b/ui/gtk/select_file_dialog_linux_gtk.cc
|
||||
index b83f0177a2adb0a19be49684f865941e6708f626..f313c766ddc2b79f082e70138dd566a846f0d923 100644
|
||||
index b83f0177a2adb0a19be49684f865941e6708f626..a8c7032cfc122b97665c41da9e1191e747b95a33 100644
|
||||
--- a/ui/gtk/select_file_dialog_linux_gtk.cc
|
||||
+++ b/ui/gtk/select_file_dialog_linux_gtk.cc
|
||||
@@ -407,9 +407,11 @@ GtkWidget* SelectFileDialogLinuxGtk::CreateFileOpenHelper(
|
||||
@@ -259,8 +259,12 @@ void SelectFileDialogLinuxGtk::SelectFileImpl(
|
||||
case SELECT_EXISTING_FOLDER:
|
||||
dialog = CreateSelectFolderDialog(type, title_string, default_path,
|
||||
owning_window);
|
||||
- connect("response",
|
||||
- &SelectFileDialogLinuxGtk::OnSelectSingleFolderDialogResponse);
|
||||
+ if (allow_multiple_selection())
|
||||
+ connect("response",
|
||||
+ &SelectFileDialogLinuxGtk::OnSelectMultiFolderDialogResponse);
|
||||
+ else
|
||||
+ connect("response",
|
||||
+ &SelectFileDialogLinuxGtk::OnSelectSingleFolderDialogResponse);
|
||||
break;
|
||||
case SELECT_OPEN_FILE:
|
||||
dialog = CreateFileOpenDialog(title_string, default_path, owning_window);
|
||||
@@ -407,9 +411,11 @@ GtkWidget* SelectFileDialogLinuxGtk::CreateFileOpenHelper(
|
||||
const std::string& title,
|
||||
const base::FilePath& default_path,
|
||||
gfx::NativeWindow parent) {
|
||||
@@ -30,7 +45,7 @@ index b83f0177a2adb0a19be49684f865941e6708f626..f313c766ddc2b79f082e70138dd566a8
|
||||
SetGtkTransientForAura(dialog, parent);
|
||||
AddFilters(GTK_FILE_CHOOSER(dialog));
|
||||
|
||||
@@ -425,6 +427,7 @@ GtkWidget* SelectFileDialogLinuxGtk::CreateFileOpenHelper(
|
||||
@@ -425,6 +431,7 @@ GtkWidget* SelectFileDialogLinuxGtk::CreateFileOpenHelper(
|
||||
GtkFileChooserSetCurrentFolder(GTK_FILE_CHOOSER(dialog),
|
||||
*last_opened_path());
|
||||
}
|
||||
@@ -38,7 +53,7 @@ index b83f0177a2adb0a19be49684f865941e6708f626..f313c766ddc2b79f082e70138dd566a8
|
||||
return dialog;
|
||||
}
|
||||
|
||||
@@ -440,11 +443,15 @@ GtkWidget* SelectFileDialogLinuxGtk::CreateSelectFolderDialog(
|
||||
@@ -440,11 +447,15 @@ GtkWidget* SelectFileDialogLinuxGtk::CreateSelectFolderDialog(
|
||||
? l10n_util::GetStringUTF8(IDS_SELECT_UPLOAD_FOLDER_DIALOG_TITLE)
|
||||
: l10n_util::GetStringUTF8(IDS_SELECT_FOLDER_DIALOG_TITLE);
|
||||
}
|
||||
@@ -59,7 +74,7 @@ index b83f0177a2adb0a19be49684f865941e6708f626..f313c766ddc2b79f082e70138dd566a8
|
||||
|
||||
GtkWidget* dialog = GtkFileChooserDialogNew(
|
||||
title_string.c_str(), nullptr, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER,
|
||||
@@ -466,7 +473,8 @@ GtkWidget* SelectFileDialogLinuxGtk::CreateSelectFolderDialog(
|
||||
@@ -466,7 +477,8 @@ GtkWidget* SelectFileDialogLinuxGtk::CreateSelectFolderDialog(
|
||||
gtk_file_filter_add_mime_type(only_folders, "inode/directory");
|
||||
gtk_file_filter_add_mime_type(only_folders, "text/directory");
|
||||
gtk_file_chooser_add_filter(chooser, only_folders);
|
||||
@@ -69,7 +84,7 @@ index b83f0177a2adb0a19be49684f865941e6708f626..f313c766ddc2b79f082e70138dd566a8
|
||||
return dialog;
|
||||
}
|
||||
|
||||
@@ -503,10 +511,11 @@ GtkWidget* SelectFileDialogLinuxGtk::CreateSaveAsDialog(
|
||||
@@ -503,10 +515,11 @@ GtkWidget* SelectFileDialogLinuxGtk::CreateSaveAsDialog(
|
||||
std::string title_string =
|
||||
!title.empty() ? title
|
||||
: l10n_util::GetStringUTF8(IDS_SAVE_AS_DIALOG_TITLE);
|
||||
@@ -83,7 +98,7 @@ index b83f0177a2adb0a19be49684f865941e6708f626..f313c766ddc2b79f082e70138dd566a8
|
||||
GTK_RESPONSE_ACCEPT);
|
||||
SetGtkTransientForAura(dialog, parent);
|
||||
|
||||
@@ -532,9 +541,10 @@ GtkWidget* SelectFileDialogLinuxGtk::CreateSaveAsDialog(
|
||||
@@ -532,9 +545,10 @@ GtkWidget* SelectFileDialogLinuxGtk::CreateSaveAsDialog(
|
||||
gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog), FALSE);
|
||||
// Overwrite confirmation is always enabled in GTK4.
|
||||
if (!GtkCheckVersion(4)) {
|
||||
@@ -96,6 +111,65 @@ index b83f0177a2adb0a19be49684f865941e6708f626..f313c766ddc2b79f082e70138dd566a8
|
||||
return dialog;
|
||||
}
|
||||
|
||||
@@ -589,15 +603,29 @@ void SelectFileDialogLinuxGtk::OnSelectSingleFolderDialogResponse(
|
||||
void SelectFileDialogLinuxGtk::OnSelectMultiFileDialogResponse(
|
||||
GtkWidget* dialog,
|
||||
int response_id) {
|
||||
+ SelectMultiFileHelper(dialog, response_id, false);
|
||||
+}
|
||||
+
|
||||
+void SelectFileDialogLinuxGtk::OnSelectMultiFolderDialogResponse(
|
||||
+ GtkWidget* dialog,
|
||||
+ int response_id) {
|
||||
+ SelectMultiFileHelper(dialog, response_id, true);
|
||||
+}
|
||||
+
|
||||
+void SelectFileDialogLinuxGtk::SelectMultiFileHelper(GtkWidget* dialog,
|
||||
+ int response_id,
|
||||
+ bool allow_folder) {
|
||||
if (IsCancelResponse(response_id)) {
|
||||
FileNotSelected(dialog);
|
||||
return;
|
||||
}
|
||||
|
||||
auto filenames = GtkFileChooserGetFilenames(dialog);
|
||||
- std::erase_if(filenames, [this](const base::FilePath& path) {
|
||||
- return CallDirectoryExistsOnUIThread(path);
|
||||
+ std::erase_if(filenames, [this, &allow_folder](const base::FilePath& path) {
|
||||
+ bool directory_exists = CallDirectoryExistsOnUIThread(path);
|
||||
+ return !allow_folder && directory_exists;
|
||||
});
|
||||
+
|
||||
if (filenames.empty()) {
|
||||
FileNotSelected(dialog);
|
||||
return;
|
||||
diff --git a/ui/gtk/select_file_dialog_linux_gtk.h b/ui/gtk/select_file_dialog_linux_gtk.h
|
||||
index 213eaa5ec6d657a659726cb38103e8bd671fe907..f497447c598198bf690758b1d1c5c6fe4112627f 100644
|
||||
--- a/ui/gtk/select_file_dialog_linux_gtk.h
|
||||
+++ b/ui/gtk/select_file_dialog_linux_gtk.h
|
||||
@@ -108,6 +108,12 @@ class SelectFileDialogLinuxGtk : public ui::SelectFileDialogLinux,
|
||||
gint response_id,
|
||||
bool allow_folder);
|
||||
|
||||
+ // Common function for OnSelectMultiFileDialogResponse and
|
||||
+ // OnSelectMultiFolderDialogResponse.
|
||||
+ void SelectMultiFileHelper(GtkWidget* dialog,
|
||||
+ gint response_id,
|
||||
+ bool allow_folder);
|
||||
+
|
||||
// Common function for CreateFileOpenDialog and CreateMultiFileOpenDialog.
|
||||
GtkWidget* CreateFileOpenHelper(const std::string& title,
|
||||
const base::FilePath& default_path,
|
||||
@@ -122,6 +128,9 @@ class SelectFileDialogLinuxGtk : public ui::SelectFileDialogLinux,
|
||||
// Callback for when the user responds to a Open Multiple Files dialog.
|
||||
void OnSelectMultiFileDialogResponse(GtkWidget* dialog, int response_id);
|
||||
|
||||
+ // Callback for when the user responds to a Select Multiple Folders dialog.
|
||||
+ void OnSelectMultiFolderDialogResponse(GtkWidget* dialog, int response_id);
|
||||
+
|
||||
// Callback for when the file chooser gets destroyed.
|
||||
void OnFileChooserDestroy(GtkWidget* dialog);
|
||||
|
||||
diff --git a/ui/shell_dialogs/select_file_dialog.h b/ui/shell_dialogs/select_file_dialog.h
|
||||
index eb3d997598631b220c3566748f23a5cdac3e4692..b4b2f7294ce6e9349a4a8a05f614e93359eca25a 100644
|
||||
--- a/ui/shell_dialogs/select_file_dialog.h
|
||||
@@ -186,18 +260,90 @@ index 61683d0eddb04c494ca5e650e7d556b44968ec49..5492456a9138b250e97a5479838bb443
|
||||
|
||||
} // namespace ui
|
||||
diff --git a/ui/shell_dialogs/select_file_dialog_linux_kde.cc b/ui/shell_dialogs/select_file_dialog_linux_kde.cc
|
||||
index 64a79ebe2e2d21d5a6b4a98042d1cdb7b6edad52..16f2ae01a8d33e6341ed52638e963c340455ebf8 100644
|
||||
index 64a79ebe2e2d21d5a6b4a98042d1cdb7b6edad52..748c2506781a237641b25b426876be14c8b7ba82 100644
|
||||
--- a/ui/shell_dialogs/select_file_dialog_linux_kde.cc
|
||||
+++ b/ui/shell_dialogs/select_file_dialog_linux_kde.cc
|
||||
@@ -468,7 +468,7 @@ void SelectFileDialogLinuxKde::CreateSelectFolderDialog(
|
||||
@@ -154,9 +154,20 @@ class SelectFileDialogLinuxKde : public SelectFileDialogLinux {
|
||||
void OnSelectMultiFileDialogResponse(
|
||||
gfx::AcceleratedWidget parent,
|
||||
std::unique_ptr<KDialogOutputParams> results);
|
||||
+
|
||||
+ // Common function for OnSelectSingleFolderDialogResponse and
|
||||
+ // OnSelectMultiFileDialogResponse.
|
||||
+ void SelectMultiFileDialogHelper(
|
||||
+ bool allow_folder,
|
||||
+ gfx::AcceleratedWidget parent,
|
||||
+ std::unique_ptr<KDialogOutputParams> results);
|
||||
+
|
||||
void OnSelectSingleFolderDialogResponse(
|
||||
gfx::AcceleratedWidget parent,
|
||||
std::unique_ptr<KDialogOutputParams> results);
|
||||
+ void OnSelectMultiFolderDialogResponse(
|
||||
+ gfx::AcceleratedWidget parent,
|
||||
+ std::unique_ptr<KDialogOutputParams> results);
|
||||
|
||||
// Should be either DESKTOP_ENVIRONMENT_KDE3, KDE4, KDE5, or KDE6.
|
||||
base::nix::DesktopEnvironment desktop_;
|
||||
@@ -461,6 +472,7 @@ void SelectFileDialogLinuxKde::CreateSelectFolderDialog(
|
||||
int title_message_id = (type == SELECT_UPLOAD_FOLDER)
|
||||
? IDS_SELECT_UPLOAD_FOLDER_DIALOG_TITLE
|
||||
: IDS_SELECT_FOLDER_DIALOG_TITLE;
|
||||
+ bool multiple_selection = allow_multiple_selection();
|
||||
pipe_task_runner_->PostTaskAndReplyWithResult(
|
||||
FROM_HERE,
|
||||
base::BindOnce(
|
||||
@@ -468,10 +480,12 @@ void SelectFileDialogLinuxKde::CreateSelectFolderDialog(
|
||||
KDialogParams(
|
||||
"--getexistingdirectory", GetTitle(title, title_message_id),
|
||||
default_path.empty() ? *last_opened_path() : default_path, parent,
|
||||
- false, false)),
|
||||
+ false, allow_multiple_selection())),
|
||||
+ false, multiple_selection)),
|
||||
base::BindOnce(
|
||||
&SelectFileDialogLinuxKde::OnSelectSingleFolderDialogResponse, this,
|
||||
parent));
|
||||
- &SelectFileDialogLinuxKde::OnSelectSingleFolderDialogResponse, this,
|
||||
- parent));
|
||||
+ multiple_selection
|
||||
+ ? &SelectFileDialogLinuxKde::OnSelectMultiFolderDialogResponse
|
||||
+ : &SelectFileDialogLinuxKde::OnSelectSingleFolderDialogResponse,
|
||||
+ this, parent));
|
||||
}
|
||||
|
||||
void SelectFileDialogLinuxKde::CreateFileOpenDialog(
|
||||
@@ -561,7 +575,8 @@ void SelectFileDialogLinuxKde::OnSelectSingleFolderDialogResponse(
|
||||
SelectSingleFileHelper(true, std::move(results));
|
||||
}
|
||||
|
||||
-void SelectFileDialogLinuxKde::OnSelectMultiFileDialogResponse(
|
||||
+void SelectFileDialogLinuxKde::SelectMultiFileDialogHelper(
|
||||
+ bool allow_folder,
|
||||
gfx::AcceleratedWidget parent,
|
||||
std::unique_ptr<KDialogOutputParams> results) {
|
||||
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
|
||||
@@ -579,7 +594,7 @@ void SelectFileDialogLinuxKde::OnSelectMultiFileDialogResponse(
|
||||
base::SplitStringPiece(results->output, "\n", base::KEEP_WHITESPACE,
|
||||
base::SPLIT_WANT_NONEMPTY)) {
|
||||
base::FilePath path(line);
|
||||
- if (CallDirectoryExistsOnUIThread(path))
|
||||
+ if (!allow_folder && CallDirectoryExistsOnUIThread(path))
|
||||
continue;
|
||||
filenames_fp.push_back(path);
|
||||
}
|
||||
@@ -591,4 +606,16 @@ void SelectFileDialogLinuxKde::OnSelectMultiFileDialogResponse(
|
||||
MultiFilesSelected(filenames_fp);
|
||||
}
|
||||
|
||||
+void SelectFileDialogLinuxKde::OnSelectMultiFolderDialogResponse(
|
||||
+ gfx::AcceleratedWidget parent,
|
||||
+ std::unique_ptr<KDialogOutputParams> results) {
|
||||
+ SelectMultiFileDialogHelper(true, parent, std::move(results));
|
||||
+}
|
||||
+
|
||||
+void SelectFileDialogLinuxKde::OnSelectMultiFileDialogResponse(
|
||||
+ gfx::AcceleratedWidget parent,
|
||||
+ std::unique_ptr<KDialogOutputParams> results) {
|
||||
+ SelectMultiFileDialogHelper(false, parent, std::move(results));
|
||||
+}
|
||||
+
|
||||
} // namespace ui
|
||||
diff --git a/ui/shell_dialogs/select_file_dialog_linux_portal.cc b/ui/shell_dialogs/select_file_dialog_linux_portal.cc
|
||||
index 9780c80ffff7bdb715a9adcb656e5d33be974db6..cf6edf5c68a8e2f8ffda119b58c6283bc43199c0 100644
|
||||
--- a/ui/shell_dialogs/select_file_dialog_linux_portal.cc
|
||||
|
||||
@@ -605,7 +605,7 @@ index 402be34ab888cdf834d0fb65de0832e9a8021ced..82ddc92a35d824926c30279e660cc4e8
|
||||
|
||||
#if BUILDFLAG(IS_CHROMEOS)
|
||||
diff --git a/chrome/browser/printing/printer_query_oop.cc b/chrome/browser/printing/printer_query_oop.cc
|
||||
index e271d17b3261c47f3dbeaf9be0b3c4229ee27181..00b906660580aca7d5aabcde54d68c2161ea71dd 100644
|
||||
index e271d17b3261c47f3dbeaf9be0b3c4229ee27181..ed19764250d9125915f0f948896acd7eae7911a6 100644
|
||||
--- a/chrome/browser/printing/printer_query_oop.cc
|
||||
+++ b/chrome/browser/printing/printer_query_oop.cc
|
||||
@@ -127,7 +127,7 @@ void PrinterQueryOop::OnDidAskUserForSettings(
|
||||
|
||||
@@ -250,8 +250,8 @@ int NodeMain() {
|
||||
|
||||
uint64_t env_flags = node::EnvironmentFlags::kDefaultFlags |
|
||||
node::EnvironmentFlags::kHideConsoleWindows;
|
||||
env = node::CreateEnvironment(
|
||||
isolate_data, isolate->GetCurrentContext(), result->args(),
|
||||
env = electron::util::CreateEnvironment(
|
||||
isolate, isolate_data, isolate->GetCurrentContext(), result->args(),
|
||||
result->exec_args(),
|
||||
static_cast<node::EnvironmentFlags::Flags>(env_flags));
|
||||
CHECK_NE(nullptr, env);
|
||||
|
||||
@@ -4,15 +4,9 @@
|
||||
|
||||
#include "shell/browser/api/electron_api_global_shortcut.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "base/containers/contains.h"
|
||||
#include "base/strings/utf_string_conversions.h"
|
||||
#include "base/uuid.h"
|
||||
#include "components/prefs/pref_service.h"
|
||||
#include "electron/shell/browser/electron_browser_context.h"
|
||||
#include "electron/shell/common/electron_constants.h"
|
||||
#include "extensions/common/command.h"
|
||||
#include "gin/dictionary.h"
|
||||
#include "gin/handle.h"
|
||||
@@ -68,12 +62,7 @@ void GlobalShortcut::OnKeyPressed(const ui::Accelerator& accelerator) {
|
||||
|
||||
void GlobalShortcut::ExecuteCommand(const extensions::ExtensionId& extension_id,
|
||||
const std::string& command_id) {
|
||||
if (!base::Contains(command_callback_map_, command_id)) {
|
||||
// This should never occur, because if it does, GlobalShortcutListener
|
||||
// notifies us with wrong command.
|
||||
NOTREACHED();
|
||||
}
|
||||
command_callback_map_[command_id].Run();
|
||||
// Ignore extension commands
|
||||
}
|
||||
|
||||
bool GlobalShortcut::RegisterAll(
|
||||
@@ -114,56 +103,13 @@ bool GlobalShortcut::Register(const ui::Accelerator& accelerator,
|
||||
}
|
||||
#endif
|
||||
|
||||
auto* instance = GlobalShortcutListener::GetInstance();
|
||||
if (!instance) {
|
||||
if (!GlobalShortcutListener::GetInstance()->RegisterAccelerator(accelerator,
|
||||
this)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (instance->IsRegistrationHandledExternally()) {
|
||||
auto* context = ElectronBrowserContext::From("", false);
|
||||
PrefService* prefs = context->prefs();
|
||||
|
||||
// Need a unique profile id. Set one if not generated yet, otherwise re-use
|
||||
// the same so that the session for the globalShortcuts is able to get
|
||||
// already registered shortcuts from the previous session. This will be used
|
||||
// by GlobalShortcutListenerLinux as a session key.
|
||||
std::string profile_id = prefs->GetString(kElectronGlobalShortcutsUuid);
|
||||
if (profile_id.empty()) {
|
||||
profile_id = base::Uuid::GenerateRandomV4().AsLowercaseString();
|
||||
prefs->SetString(kElectronGlobalShortcutsUuid, profile_id);
|
||||
}
|
||||
|
||||
// There is no way to get command id for the accelerator as it's extensions'
|
||||
// thing. Instead, we can convert it to string in a following example form
|
||||
// - std::string("Alt+Shift+K"). That must be sufficient enough for us to
|
||||
// map this accelerator with registered commands.
|
||||
const std::string command_str =
|
||||
extensions::Command::AcceleratorToString(accelerator);
|
||||
ui::CommandMap commands;
|
||||
extensions::Command command(
|
||||
command_str, base::UTF8ToUTF16("Electron shortcut " + command_str),
|
||||
/*accelerator=*/std::string(), /*global=*/true);
|
||||
command.set_accelerator(accelerator);
|
||||
commands[command_str] = command;
|
||||
|
||||
// In order to distinguish the shortcuts, we must register multiple commands
|
||||
// as different extensions. Otherwise, each shortcut will be an alternative
|
||||
// for the very first registered and we'll not be able to distinguish them.
|
||||
// For example, if Alt+Shift+K is registered first, registering and pressing
|
||||
// Alt+Shift+M will trigger global shortcuts, but the command id that is
|
||||
// received by GlobalShortcut will correspond to Alt+Shift+K as our command
|
||||
// id is basically a stringified accelerator.
|
||||
const std::string fake_extension_id = command_str + "+" + profile_id;
|
||||
instance->OnCommandsChanged(fake_extension_id, profile_id, commands, this);
|
||||
command_callback_map_[command_str] = callback;
|
||||
return true;
|
||||
} else {
|
||||
if (instance->RegisterAccelerator(accelerator, this)) {
|
||||
accelerator_callback_map_[accelerator] = callback;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
accelerator_callback_map_[accelerator] = callback;
|
||||
return true;
|
||||
}
|
||||
|
||||
void GlobalShortcut::Unregister(const ui::Accelerator& accelerator) {
|
||||
@@ -181,10 +127,8 @@ void GlobalShortcut::Unregister(const ui::Accelerator& accelerator) {
|
||||
}
|
||||
#endif
|
||||
|
||||
if (GlobalShortcutListener::GetInstance()) {
|
||||
GlobalShortcutListener::GetInstance()->UnregisterAccelerator(accelerator,
|
||||
this);
|
||||
}
|
||||
GlobalShortcutListener::GetInstance()->UnregisterAccelerator(accelerator,
|
||||
this);
|
||||
}
|
||||
|
||||
void GlobalShortcut::UnregisterSome(
|
||||
@@ -195,12 +139,7 @@ void GlobalShortcut::UnregisterSome(
|
||||
}
|
||||
|
||||
bool GlobalShortcut::IsRegistered(const ui::Accelerator& accelerator) {
|
||||
if (base::Contains(accelerator_callback_map_, accelerator)) {
|
||||
return true;
|
||||
}
|
||||
const std::string command_str =
|
||||
extensions::Command::AcceleratorToString(accelerator);
|
||||
return base::Contains(command_callback_map_, command_str);
|
||||
return base::Contains(accelerator_callback_map_, accelerator);
|
||||
}
|
||||
|
||||
void GlobalShortcut::UnregisterAll() {
|
||||
@@ -210,9 +149,7 @@ void GlobalShortcut::UnregisterAll() {
|
||||
return;
|
||||
}
|
||||
accelerator_callback_map_.clear();
|
||||
if (GlobalShortcutListener::GetInstance()) {
|
||||
GlobalShortcutListener::GetInstance()->UnregisterAccelerators(this);
|
||||
}
|
||||
GlobalShortcutListener::GetInstance()->UnregisterAccelerators(this);
|
||||
}
|
||||
|
||||
// static
|
||||
|
||||
@@ -44,7 +44,6 @@ class GlobalShortcut final
|
||||
private:
|
||||
typedef std::map<ui::Accelerator, base::RepeatingClosure>
|
||||
AcceleratorCallbackMap;
|
||||
typedef std::map<std::string, base::RepeatingClosure> CommandCallbackMap;
|
||||
|
||||
bool RegisterAll(const std::vector<ui::Accelerator>& accelerators,
|
||||
const base::RepeatingClosure& callback);
|
||||
@@ -61,7 +60,6 @@ class GlobalShortcut final
|
||||
const std::string& command_id) override;
|
||||
|
||||
AcceleratorCallbackMap accelerator_callback_map_;
|
||||
CommandCallbackMap command_callback_map_;
|
||||
};
|
||||
|
||||
} // namespace electron::api
|
||||
|
||||
@@ -236,6 +236,10 @@ const char* Notification::GetTypeName() {
|
||||
return GetClassName();
|
||||
}
|
||||
|
||||
void Notification::WillBeDestroyed() {
|
||||
ClearWeak();
|
||||
}
|
||||
|
||||
} // namespace electron::api
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -57,6 +57,9 @@ class Notification final : public gin::Wrappable<Notification>,
|
||||
static gin::WrapperInfo kWrapperInfo;
|
||||
const char* GetTypeName() override;
|
||||
|
||||
// gin_helper::CleanedUpAtExit
|
||||
void WillBeDestroyed() override;
|
||||
|
||||
// disable copy
|
||||
Notification(const Notification&) = delete;
|
||||
Notification& operator=(const Notification&) = delete;
|
||||
|
||||
@@ -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(running_status);
|
||||
|
||||
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,
|
||||
content::StatusCodeResponse status) {
|
||||
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,
|
||||
content::StatusCodeResponse status);
|
||||
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};
|
||||
};
|
||||
|
||||
|
||||
359
shell/browser/api/electron_api_service_worker_main.cc
Normal file
359
shell/browser/api/electron_api_service_worker_main.cc
Normal file
@@ -0,0 +1,359 @@
|
||||
// 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();
|
||||
MaybeDisconnectRemote();
|
||||
GetVersionIdMap().erase(key_);
|
||||
Unpin();
|
||||
}
|
||||
|
||||
void ServiceWorkerMain::MaybeDisconnectRemote() {
|
||||
if (remote_.is_bound() &&
|
||||
(version_destroyed_ ||
|
||||
(!service_worker_context_->IsLiveStartingServiceWorker(version_id_) &&
|
||||
!service_worker_context_->IsLiveRunningServiceWorker(version_id_)))) {
|
||||
remote_.reset();
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
blink::EmbeddedWorkerStatus running_status) {
|
||||
// Disconnect remote when content::ServiceWorkerHost has terminated.
|
||||
MaybeDisconnectRemote();
|
||||
|
||||
InvalidateVersionInfo();
|
||||
|
||||
// Redundant worker has been marked for deletion. Now that it's stopped, let's
|
||||
// destroy our wrapper.
|
||||
if (redundant_ && running_status == blink::EmbeddedWorkerStatus::kStopped) {
|
||||
Destroy();
|
||||
}
|
||||
}
|
||||
|
||||
void ServiceWorkerMain::OnVersionRedundant() {
|
||||
// Redundant service workers have been either unregistered or replaced. A new
|
||||
// ServiceWorkerMain will need to be created.
|
||||
// Set internal state to mark it for deletion once it has fully stopped.
|
||||
redundant_ = true;
|
||||
}
|
||||
|
||||
bool ServiceWorkerMain::IsDestroyed() const {
|
||||
return version_destroyed_;
|
||||
}
|
||||
|
||||
const blink::StorageKey ServiceWorkerMain::GetStorageKey() {
|
||||
GURL scope = version_info_ ? version_info()->scope : GURL::EmptyGURL();
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
DCHECK(service_worker_context_);
|
||||
if (!service_worker_context_)
|
||||
return;
|
||||
|
||||
content::ServiceWorkerExternalRequestResult result =
|
||||
service_worker_context_->FinishedExternalRequest(version_id_,
|
||||
request_uuid);
|
||||
|
||||
std::string error;
|
||||
switch (result) {
|
||||
case content::ServiceWorkerExternalRequestResult::kOk:
|
||||
break;
|
||||
case content::ServiceWorkerExternalRequestResult::kBadRequestId:
|
||||
error = "Unknown external request UUID";
|
||||
break;
|
||||
case content::ServiceWorkerExternalRequestResult::kWorkerNotRunning:
|
||||
error = "Service worker is no longer running";
|
||||
break;
|
||||
case content::ServiceWorkerExternalRequestResult::kWorkerNotFound:
|
||||
error = "Service worker was not found";
|
||||
break;
|
||||
case content::ServiceWorkerExternalRequestResult::kNullContext:
|
||||
default:
|
||||
error = "Service worker context is unavailable and may be shutting down";
|
||||
break;
|
||||
}
|
||||
|
||||
if (!error.empty()) {
|
||||
isolate->ThrowException(
|
||||
v8::Exception::TypeError(gin::StringToV8(isolate, error)));
|
||||
}
|
||||
}
|
||||
|
||||
size_t ServiceWorkerMain::CountExternalRequestsForTest() {
|
||||
if (version_destroyed_)
|
||||
return 0;
|
||||
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::CountExternalRequestsForTest)
|
||||
.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)
|
||||
178
shell/browser/api/electron_api_service_worker_main.h
Normal file
178
shell/browser/api/electron_api_service_worker_main.h
Normal file
@@ -0,0 +1,178 @@
|
||||
// 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(blink::EmbeddedWorkerStatus running_status);
|
||||
void OnVersionRedundant();
|
||||
|
||||
protected:
|
||||
explicit ServiceWorkerMain(content::ServiceWorkerContext* sw_context,
|
||||
int64_t version_id,
|
||||
const ServiceWorkerKey& key);
|
||||
~ServiceWorkerMain() override;
|
||||
|
||||
private:
|
||||
void Destroy();
|
||||
void MaybeDisconnectRemote();
|
||||
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 CountExternalRequestsForTest();
|
||||
|
||||
// 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;
|
||||
|
||||
// Whether the Service Worker version's state is redundant.
|
||||
bool redundant_ = 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_
|
||||
@@ -1065,16 +1065,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();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1800,8 +1856,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)
|
||||
@@ -1852,6 +1909,10 @@ const char* Session::GetTypeName() {
|
||||
return GetClassName();
|
||||
}
|
||||
|
||||
void Session::WillBeDestroyed() {
|
||||
ClearWeak();
|
||||
}
|
||||
|
||||
} // namespace electron::api
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -57,6 +57,7 @@ class ProxyConfig;
|
||||
namespace electron {
|
||||
|
||||
class ElectronBrowserContext;
|
||||
struct PreloadScript;
|
||||
|
||||
namespace api {
|
||||
|
||||
@@ -102,6 +103,9 @@ class Session final : public gin::Wrappable<Session>,
|
||||
static const char* GetClassName() { return "Session"; }
|
||||
const char* GetTypeName() override;
|
||||
|
||||
// gin_helper::CleanedUpAtExit
|
||||
void WillBeDestroyed() override;
|
||||
|
||||
// Methods.
|
||||
v8::Local<v8::Promise> ResolveHost(
|
||||
std::string host,
|
||||
@@ -138,8 +142,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();
|
||||
|
||||
@@ -431,6 +431,10 @@ const char* Tray::GetTypeName() {
|
||||
return GetClassName();
|
||||
}
|
||||
|
||||
void Tray::WillBeDestroyed() {
|
||||
ClearWeak();
|
||||
}
|
||||
|
||||
} // namespace electron::api
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -58,6 +58,9 @@ class Tray final : public gin::Wrappable<Tray>,
|
||||
static gin::WrapperInfo kWrapperInfo;
|
||||
const char* GetTypeName() override;
|
||||
|
||||
// gin_helper::CleanedUpAtExit
|
||||
void WillBeDestroyed() override;
|
||||
|
||||
// disable copy
|
||||
Tray(const Tray&) = delete;
|
||||
Tray& operator=(const Tray&) = delete;
|
||||
|
||||
@@ -3757,16 +3757,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(
|
||||
@@ -4520,7 +4519,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)
|
||||
@@ -4570,6 +4569,10 @@ const char* WebContents::GetTypeName() {
|
||||
return GetClassName();
|
||||
}
|
||||
|
||||
void WebContents::WillBeDestroyed() {
|
||||
ClearWeak();
|
||||
}
|
||||
|
||||
ElectronBrowserContext* WebContents::GetBrowserContext() const {
|
||||
return static_cast<ElectronBrowserContext*>(
|
||||
web_contents()->GetBrowserContext());
|
||||
|
||||
@@ -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"
|
||||
@@ -179,6 +180,9 @@ class WebContents final : public ExclusiveAccessContext,
|
||||
static gin::WrapperInfo kWrapperInfo;
|
||||
const char* GetTypeName() override;
|
||||
|
||||
// gin_helper::CleanedUpAtExit
|
||||
void WillBeDestroyed() override;
|
||||
|
||||
void Destroy();
|
||||
void Close(std::optional<gin_helper::Dictionary> options);
|
||||
base::WeakPtr<WebContents> GetWeakPtr() { return weak_factory_.GetWeakPtr(); }
|
||||
@@ -341,8 +345,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;
|
||||
|
||||
@@ -306,6 +306,10 @@ const char* MessagePort::GetTypeName() {
|
||||
return "MessagePort";
|
||||
}
|
||||
|
||||
void MessagePort::WillBeDestroyed() {
|
||||
ClearWeak();
|
||||
}
|
||||
|
||||
} // namespace electron
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -61,6 +61,9 @@ class MessagePort final : public gin::Wrappable<MessagePort>,
|
||||
v8::Isolate* isolate) override;
|
||||
const char* GetTypeName() override;
|
||||
|
||||
// gin_helper::CleanedUpAtExit
|
||||
void WillBeDestroyed() override;
|
||||
|
||||
private:
|
||||
MessagePort();
|
||||
|
||||
|
||||
@@ -458,10 +458,6 @@ void ElectronBrowserContext::InitPrefs() {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Unique uuid for global shortcuts.
|
||||
registry->RegisterStringPref(electron::kElectronGlobalShortcutsUuid,
|
||||
std::string());
|
||||
}
|
||||
|
||||
void ElectronBrowserContext::SetUserAgent(const std::string& user_agent) {
|
||||
|
||||
@@ -132,11 +132,16 @@ v8::Isolate* JavascriptEnvironment::GetIsolate() {
|
||||
void JavascriptEnvironment::CreateMicrotasksRunner() {
|
||||
DCHECK(!microtasks_runner_);
|
||||
microtasks_runner_ = std::make_unique<MicrotasksRunner>(isolate());
|
||||
isolate_holder_.WillCreateMicrotasksRunner();
|
||||
base::CurrentThread::Get()->AddTaskObserver(microtasks_runner_.get());
|
||||
}
|
||||
|
||||
void JavascriptEnvironment::DestroyMicrotasksRunner() {
|
||||
DCHECK(microtasks_runner_);
|
||||
// Should be called before running gin_helper::CleanedUpAtExit::DoCleanup.
|
||||
// This helps to signal wrappable finalizer callbacks to not act on freed
|
||||
// parameters.
|
||||
isolate_holder_.WillDestroyMicrotasksRunner();
|
||||
{
|
||||
v8::HandleScope scope(isolate_);
|
||||
gin_helper::CleanedUpAtExit::DoCleanup();
|
||||
|
||||
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_
|
||||
@@ -64,5 +64,7 @@
|
||||
<string>${DEFAULT_APP_ASAR_HEADER_SHA}</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>NSPrefersDisplaySafeAreaCompatibilityMode</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -30,22 +30,4 @@ 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;
|
||||
}
|
||||
|
||||
} // 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,12 @@ 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_; }
|
||||
|
||||
private:
|
||||
SessionPreferences();
|
||||
@@ -38,7 +34,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
|
||||
|
||||
@@ -819,4 +819,8 @@ const char* SimpleURLLoaderWrapper::GetTypeName() {
|
||||
return "SimpleURLLoaderWrapper";
|
||||
}
|
||||
|
||||
void SimpleURLLoaderWrapper::WillBeDestroyed() {
|
||||
ClearWeak();
|
||||
}
|
||||
|
||||
} // namespace electron::api
|
||||
|
||||
@@ -66,6 +66,9 @@ class SimpleURLLoaderWrapper final
|
||||
v8::Isolate* isolate) override;
|
||||
const char* GetTypeName() override;
|
||||
|
||||
// gin_helper::CleanedUpAtExit
|
||||
void WillBeDestroyed() override;
|
||||
|
||||
private:
|
||||
SimpleURLLoaderWrapper(ElectronBrowserContext* browser_context,
|
||||
std::unique_ptr<network::ResourceRequest> request,
|
||||
|
||||
@@ -23,13 +23,6 @@ inline constexpr std::string_view kDeviceSerialNumberKey = "serialNumber";
|
||||
|
||||
inline constexpr base::cstring_view kRunAsNode = "ELECTRON_RUN_AS_NODE";
|
||||
|
||||
// Per-profile UUID to distinguish global shortcut sessions for
|
||||
// org.freedesktop.portal.GlobalShortcuts. This is a counterpart to
|
||||
// extensions::pref_names::kGlobalShortcutsUuid, which may be not defined
|
||||
// if extensions are disabled.
|
||||
inline constexpr char kElectronGlobalShortcutsUuid[] =
|
||||
"electron.global_shortcuts.uuid";
|
||||
|
||||
#if BUILDFLAG(ENABLE_PDF_VIEWER)
|
||||
inline constexpr std::string_view kPDFExtensionPluginName =
|
||||
"Chromium PDF Viewer";
|
||||
|
||||
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_
|
||||
@@ -27,11 +27,14 @@ CleanedUpAtExit::~CleanedUpAtExit() {
|
||||
std::erase(GetDoomed(), this);
|
||||
}
|
||||
|
||||
void CleanedUpAtExit::WillBeDestroyed() {}
|
||||
|
||||
// static
|
||||
void CleanedUpAtExit::DoCleanup() {
|
||||
auto& doomed = GetDoomed();
|
||||
while (!doomed.empty()) {
|
||||
CleanedUpAtExit* next = doomed.back();
|
||||
next->WillBeDestroyed();
|
||||
delete next;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ class CleanedUpAtExit {
|
||||
CleanedUpAtExit();
|
||||
virtual ~CleanedUpAtExit();
|
||||
|
||||
virtual void WillBeDestroyed();
|
||||
|
||||
static void DoCleanup();
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "shell/common/gin_helper/wrappable.h"
|
||||
|
||||
#include "gin/public/isolate_holder.h"
|
||||
#include "shell/common/gin_helper/dictionary.h"
|
||||
#include "v8/include/v8-function.h"
|
||||
|
||||
@@ -60,8 +61,10 @@ void WrappableBase::InitWith(v8::Isolate* isolate,
|
||||
// static
|
||||
void WrappableBase::FirstWeakCallback(
|
||||
const v8::WeakCallbackInfo<WrappableBase>& data) {
|
||||
auto* wrappable = static_cast<WrappableBase*>(data.GetInternalField(0));
|
||||
if (wrappable) {
|
||||
WrappableBase* wrappable = data.GetParameter();
|
||||
auto* wrappable_from_field =
|
||||
static_cast<WrappableBase*>(data.GetInternalField(0));
|
||||
if (wrappable && wrappable == wrappable_from_field) {
|
||||
wrappable->wrapper_.Reset();
|
||||
data.SetSecondPassCallback(SecondWeakCallback);
|
||||
}
|
||||
@@ -70,6 +73,9 @@ void WrappableBase::FirstWeakCallback(
|
||||
// static
|
||||
void WrappableBase::SecondWeakCallback(
|
||||
const v8::WeakCallbackInfo<WrappableBase>& data) {
|
||||
if (gin::IsolateHolder::DestroyedMicrotasksRunner()) {
|
||||
return;
|
||||
}
|
||||
delete static_cast<WrappableBase*>(data.GetInternalField(0));
|
||||
}
|
||||
|
||||
|
||||
@@ -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) \
|
||||
@@ -649,7 +650,6 @@ std::shared_ptr<node::Environment> NodeBindings::CreateEnvironment(
|
||||
context->SetAlignedPointerInEmbedderData(kElectronContextEmbedderDataIndex,
|
||||
static_cast<void*>(isolate_data));
|
||||
|
||||
node::Environment* env;
|
||||
uint64_t env_flags = node::EnvironmentFlags::kDefaultFlags |
|
||||
node::EnvironmentFlags::kHideConsoleWindows |
|
||||
node::EnvironmentFlags::kNoGlobalSearchPaths |
|
||||
@@ -675,24 +675,10 @@ std::shared_ptr<node::Environment> NodeBindings::CreateEnvironment(
|
||||
env_flags |= node::EnvironmentFlags::kNoStartDebugSignalHandler;
|
||||
}
|
||||
|
||||
{
|
||||
v8::TryCatch try_catch(isolate);
|
||||
env = node::CreateEnvironment(
|
||||
static_cast<node::IsolateData*>(isolate_data), context, args, exec_args,
|
||||
static_cast<node::EnvironmentFlags::Flags>(env_flags));
|
||||
|
||||
if (try_catch.HasCaught()) {
|
||||
std::string err_msg =
|
||||
"Failed to initialize node environment in process: " + process_type;
|
||||
v8::Local<v8::Message> message = try_catch.Message();
|
||||
std::string msg;
|
||||
if (!message.IsEmpty() &&
|
||||
gin::ConvertFromV8(isolate, message->Get(), &msg))
|
||||
err_msg += " , with error: " + msg;
|
||||
LOG(ERROR) << err_msg;
|
||||
}
|
||||
}
|
||||
|
||||
node::Environment* env = electron::util::CreateEnvironment(
|
||||
isolate, static_cast<node::IsolateData*>(isolate_data), context, args,
|
||||
exec_args, static_cast<node::EnvironmentFlags::Flags>(env_flags),
|
||||
process_type);
|
||||
DCHECK(env);
|
||||
|
||||
node::IsolateSettings is;
|
||||
|
||||
@@ -5,7 +5,12 @@
|
||||
#include "shell/common/node_util.h"
|
||||
|
||||
#include "base/compiler_specific.h"
|
||||
#include "base/containers/to_value_list.h"
|
||||
#include "base/json/json_writer.h"
|
||||
#include "base/logging.h"
|
||||
#include "base/strings/strcat.h"
|
||||
#include "base/strings/string_number_conversions.h"
|
||||
#include "base/values.h"
|
||||
#include "gin/converter.h"
|
||||
#include "gin/dictionary.h"
|
||||
#include "shell/browser/javascript_environment.h"
|
||||
@@ -76,4 +81,51 @@ base::span<uint8_t> as_byte_span(v8::Local<v8::Value> node_buffer) {
|
||||
return UNSAFE_BUFFERS(base::span{data, size});
|
||||
}
|
||||
|
||||
node::Environment* CreateEnvironment(v8::Isolate* isolate,
|
||||
node::IsolateData* isolate_data,
|
||||
v8::Local<v8::Context> context,
|
||||
const std::vector<std::string>& args,
|
||||
const std::vector<std::string>& exec_args,
|
||||
node::EnvironmentFlags::Flags env_flags,
|
||||
std::string_view process_type) {
|
||||
v8::TryCatch try_catch{isolate};
|
||||
node::Environment* env = node::CreateEnvironment(isolate_data, context, args,
|
||||
exec_args, env_flags);
|
||||
if (auto message = try_catch.Message(); !message.IsEmpty()) {
|
||||
base::Value::Dict dict;
|
||||
|
||||
if (std::string str; gin::ConvertFromV8(isolate, message->Get(), &str))
|
||||
dict.Set("message", std::move(str));
|
||||
|
||||
if (std::string str; gin::ConvertFromV8(
|
||||
isolate, message->GetScriptOrigin().ResourceName(), &str)) {
|
||||
const auto line_num = message->GetLineNumber(context).FromJust();
|
||||
const auto line_str = base::NumberToString(line_num);
|
||||
dict.Set("location", base::StrCat({", at ", str, ":", line_str}));
|
||||
}
|
||||
|
||||
if (std::string str; gin::ConvertFromV8(
|
||||
isolate, message->GetSourceLine(context).ToLocalChecked(), &str))
|
||||
dict.Set("source_line", std::move(str));
|
||||
|
||||
if (!std::empty(process_type))
|
||||
dict.Set("process_type", process_type);
|
||||
|
||||
if (auto list = base::ToValueList(args); !std::empty(list))
|
||||
dict.Set("args", std::move(list));
|
||||
|
||||
if (auto list = base::ToValueList(exec_args); !std::empty(list))
|
||||
dict.Set("exec_args", std::move(list));
|
||||
|
||||
std::string errstr = "Failed to initialize Node.js.";
|
||||
if (std::optional<std::string> jsonstr = base::WriteJsonWithOptions(
|
||||
dict, base::JsonOptions::OPTIONS_PRETTY_PRINT))
|
||||
errstr += base::StrCat({" ", *jsonstr});
|
||||
|
||||
LOG(ERROR) << errstr;
|
||||
}
|
||||
|
||||
return env;
|
||||
}
|
||||
|
||||
} // namespace electron::util
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#ifndef ELECTRON_SHELL_COMMON_NODE_UTIL_H_
|
||||
#define ELECTRON_SHELL_COMMON_NODE_UTIL_H_
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
@@ -13,7 +14,13 @@
|
||||
|
||||
namespace node {
|
||||
class Environment;
|
||||
class IsolateData;
|
||||
struct ThreadId;
|
||||
|
||||
namespace EnvironmentFlags {
|
||||
enum Flags : uint64_t;
|
||||
}
|
||||
} // namespace node
|
||||
|
||||
namespace electron::util {
|
||||
|
||||
@@ -37,6 +44,15 @@ v8::MaybeLocal<v8::Value> CompileAndCall(
|
||||
std::vector<v8::Local<v8::String>>* parameters,
|
||||
std::vector<v8::Local<v8::Value>>* arguments);
|
||||
|
||||
// Wrapper for node::CreateEnvironment that logs failure
|
||||
node::Environment* CreateEnvironment(v8::Isolate* isolate,
|
||||
node::IsolateData* isolate_data,
|
||||
v8::Local<v8::Context> context,
|
||||
const std::vector<std::string>& args,
|
||||
const std::vector<std::string>& exec_args,
|
||||
node::EnvironmentFlags::Flags env_flags,
|
||||
std::string_view process_type = "");
|
||||
|
||||
// Convenience function to view a Node buffer's data as a base::span().
|
||||
// Analogous to base::as_byte_span()
|
||||
[[nodiscard]] base::span<uint8_t> as_byte_span(
|
||||
|
||||
@@ -13,11 +13,14 @@
|
||||
|
||||
#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"
|
||||
@@ -25,6 +28,7 @@
|
||||
#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 +137,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 +159,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 +262,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 +269,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 +309,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 +316,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 +382,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 +398,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 +453,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 +505,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 +516,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 +583,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 +618,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 +662,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 +691,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 +706,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 +724,70 @@ 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 {
|
||||
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 +819,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 +859,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 +868,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 +882,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 +1097,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
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -16,8 +16,7 @@ import { setTimeout } from 'node:timers/promises';
|
||||
import * as nodeUrl from 'node:url';
|
||||
|
||||
import { emittedUntil, emittedNTimes } from './lib/events-helpers';
|
||||
import { debugPreviewImage } from './lib/image-helpers';
|
||||
import { HexColors, hasCapturableScreen, ScreenCapture, getPixelColor } from './lib/screen-helpers';
|
||||
import { HexColors, hasCapturableScreen, ScreenCapture } from './lib/screen-helpers';
|
||||
import { ifit, ifdescribe, defer, listen, waitUntil } from './lib/spec-helpers';
|
||||
import { closeWindow, closeAllWindows } from './lib/window-helpers';
|
||||
|
||||
@@ -1273,7 +1272,6 @@ describe('BrowserWindow module', () => {
|
||||
// We first need to resign app focus for this test to work
|
||||
const isInactive = once(app, 'did-resign-active');
|
||||
childProcess.execSync('osascript -e \'tell application "Finder" to activate\'');
|
||||
defer(() => childProcess.execSync('osascript -e \'tell application "Finder" to quit\''));
|
||||
await isInactive;
|
||||
|
||||
// Create new window
|
||||
@@ -6460,51 +6458,12 @@ describe('BrowserWindow module', () => {
|
||||
const [, , data] = await paint;
|
||||
expect(data.constructor.name).to.equal('NativeImage');
|
||||
expect(data.isEmpty()).to.be.false('data is empty');
|
||||
await debugPreviewImage(data);
|
||||
const size = data.getSize();
|
||||
const { scaleFactor } = screen.getPrimaryDisplay();
|
||||
expect(size.width).to.be.closeTo(100 * scaleFactor, 2);
|
||||
expect(size.height).to.be.closeTo(100 * scaleFactor, 2);
|
||||
});
|
||||
|
||||
it('creates offscreen window with opaque background', async () => {
|
||||
w.setBackgroundColor(HexColors.RED);
|
||||
await waitUntil(async () => {
|
||||
const paint = once(w.webContents, 'paint') as Promise<[any, Electron.Rectangle, Electron.NativeImage]>;
|
||||
w.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html'));
|
||||
const [, , data] = await paint;
|
||||
await debugPreviewImage(data);
|
||||
expect(getPixelColor(data, { x: 0, y: 0 }, true)).to.equal('#ff0000ff');
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
it('creates offscreen window with transparent background', async () => {
|
||||
w.setBackgroundColor(HexColors.TRANSPARENT);
|
||||
await waitUntil(async () => {
|
||||
const paint = once(w.webContents, 'paint') as Promise<[any, Electron.Rectangle, Electron.NativeImage]>;
|
||||
w.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html'));
|
||||
const [, , data] = await paint;
|
||||
await debugPreviewImage(data);
|
||||
expect(getPixelColor(data, { x: 0, y: 0 }, true)).to.equal(HexColors.TRANSPARENT);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
// Semi-transparent background is not supported
|
||||
it.skip('creates offscreen window with semi-transparent background', async () => {
|
||||
const bgColor = '#66ffffff'; // ARGB
|
||||
w.setBackgroundColor(bgColor);
|
||||
await waitUntil(async () => {
|
||||
const paint = once(w.webContents, 'paint') as Promise<[any, Electron.Rectangle, Electron.NativeImage]>;
|
||||
w.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html'));
|
||||
const [, , data] = await paint;
|
||||
await debugPreviewImage(data);
|
||||
expect(getPixelColor(data, { x: 0, y: 0 }, true)).to.equal(bgColor);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
it('does not crash after navigation', () => {
|
||||
w.webContents.loadURL('about:blank');
|
||||
w.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html'));
|
||||
|
||||
@@ -99,8 +99,7 @@ ifdescribe(!(['arm', 'arm64'].includes(process.arch)) || (process.platform !== '
|
||||
this.timeout(5e3);
|
||||
}
|
||||
|
||||
// FIXME(samuelmaddock): this test regularly flakes
|
||||
it.skip('does not crash on empty string', async () => {
|
||||
it('does not crash on empty string', async () => {
|
||||
const options = {
|
||||
categoryFilter: '*',
|
||||
traceOptions: 'record-until-full,enable-sampling'
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
291
spec/api-service-worker-main-spec.ts
Normal file
291
spec/api-service-worker-main-spec.ts
Normal file
@@ -0,0 +1,291 @@
|
||||
import { 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 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);
|
||||
});
|
||||
serviceWorkers.on('running-status-changed', ({ versionId, runningStatus }) => {
|
||||
console.log(`version ${versionId} is ${runningStatus}`);
|
||||
});
|
||||
}
|
||||
|
||||
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', ({ message }) => {
|
||||
console.log(message);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (!wc.isDestroyed()) wc.destroy();
|
||||
server.close();
|
||||
});
|
||||
|
||||
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!;
|
||||
}
|
||||
|
||||
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 function () {
|
||||
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}/`);
|
||||
});
|
||||
});
|
||||
});
|
||||
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>
|
||||
|
||||
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();
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
import { BaseWindow, NativeImage } from 'electron';
|
||||
|
||||
import { once } from 'node:events';
|
||||
|
||||
/**
|
||||
* Opens a window to display a native image. Useful for quickly debugging tests
|
||||
* rather than writing a file and opening manually.
|
||||
*
|
||||
* Set the `DEBUG_PREVIEW_IMAGE` environment variable to show previews.
|
||||
*/
|
||||
export async function debugPreviewImage (image: NativeImage) {
|
||||
if (!process.env.DEBUG_PREVIEW_IMAGE) return;
|
||||
const previewWindow = new BaseWindow({
|
||||
title: 'NativeImage preview',
|
||||
backgroundColor: '#444444'
|
||||
});
|
||||
const ImageView = (require('electron') as any).ImageView;
|
||||
const imgView = new ImageView();
|
||||
imgView.setImage(image);
|
||||
previewWindow.contentView.addChildView(imgView);
|
||||
imgView.setBounds({ x: 0, y: 0, ...image.getSize() });
|
||||
await once(previewWindow, 'close');
|
||||
};
|
||||
@@ -10,7 +10,6 @@ export enum HexColors {
|
||||
RED = '#ff0000',
|
||||
BLUE = '#0000ff',
|
||||
WHITE = '#ffffff',
|
||||
TRANSPARENT = '#00000000'
|
||||
}
|
||||
|
||||
function hexToRgba (
|
||||
@@ -36,10 +35,9 @@ function formatHexByte (val: number): string {
|
||||
/**
|
||||
* Get the hex color at the given pixel coordinate in an image.
|
||||
*/
|
||||
export function getPixelColor (
|
||||
function getPixelColor (
|
||||
image: Electron.NativeImage,
|
||||
point: Electron.Point,
|
||||
includeAlpha: boolean = false
|
||||
point: Electron.Point
|
||||
): string {
|
||||
// image.crop crashes if point is fractional, so round to prevent that crash
|
||||
const pixel = image.crop({
|
||||
@@ -50,10 +48,8 @@ export function getPixelColor (
|
||||
});
|
||||
// TODO(samuelmaddock): NativeImage.toBitmap() should return the raw pixel
|
||||
// color, but it sometimes differs. Why is that?
|
||||
const [b, g, r, a] = pixel.toBitmap();
|
||||
let hex = `#${formatHexByte(r)}${formatHexByte(g)}${formatHexByte(b)}`;
|
||||
if (includeAlpha) hex += `${formatHexByte(a)}`;
|
||||
return hex;
|
||||
const [b, g, r] = pixel.toBitmap();
|
||||
return `#${formatHexByte(r)}${formatHexByte(g)}${formatHexByte(b)}`;
|
||||
}
|
||||
|
||||
/** Calculate euclidean distance between colors. */
|
||||
@@ -72,7 +68,7 @@ function colorDistance (hexColorA: string, hexColorB: string): number {
|
||||
* Determine if colors are similar based on distance. This can be useful when
|
||||
* comparing colors which may differ based on lossy compression.
|
||||
*/
|
||||
export function areColorsSimilar (
|
||||
function areColorsSimilar (
|
||||
hexColorA: string,
|
||||
hexColorB: string,
|
||||
distanceThreshold = 90
|
||||
|
||||
5
typings/internal-ambient.d.ts
vendored
5
typings/internal-ambient.d.ts
vendored
@@ -111,6 +111,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 +232,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 };
|
||||
|
||||
20
typings/internal-electron.d.ts
vendored
20
typings/internal-electron.d.ts
vendored
@@ -63,10 +63,21 @@ 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 {
|
||||
_startExternalRequest(hasTimeout: boolean): { id: string, ok: boolean };
|
||||
_finishExternalRequest(uuid: string): void;
|
||||
_countExternalRequests(): number;
|
||||
}
|
||||
|
||||
|
||||
interface TouchBar {
|
||||
_removeFromWindow: (win: BaseWindow) => void;
|
||||
}
|
||||
@@ -76,7 +87,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;
|
||||
@@ -330,6 +341,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