Compare commits

..

27 Commits

Author SHA1 Message Date
Sam Maddock
cc6164fe27 feat: ServiceWorkerMain (#45341) 2025-01-31 12:40:19 -05:00
trop[bot]
59e4794ff5 build: fix slack-github-action for backports (#45403)
build: fix slack-github-action for backports

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2025-01-31 09:53:50 -05:00
trop[bot]
fa03b92f7e feat: contextBridge.executeInMainWorld (#45330)
Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Samuel Maddock <samuel.maddock@gmail.com>
2025-01-31 09:50:44 -05:00
trop[bot]
9d696ceffe feat: redesign preload APIs (#45329)
* feat: redesign preload APIs

Co-authored-by: Samuel Maddock <samuel.maddock@gmail.com>

* docs: remove service-worker mentions for now

Co-authored-by: Samuel Maddock <samuel.maddock@gmail.com>

* fix lint

Co-authored-by: Samuel Maddock <samuel.maddock@gmail.com>

* remove service-worker ipc code

Co-authored-by: Samuel Maddock <samuel.maddock@gmail.com>

* add filename

Co-authored-by: Samuel Maddock <samuel.maddock@gmail.com>

* fix: web preferences preload not included

Co-authored-by: Samuel Maddock <samuel.maddock@gmail.com>

* fix: missing common init

Co-authored-by: Samuel Maddock <samuel.maddock@gmail.com>

* fix: preload bundle script error

Co-authored-by: Samuel Maddock <samuel.maddock@gmail.com>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Samuel Maddock <samuel.maddock@gmail.com>
2025-01-31 09:46:17 -05:00
trop[bot]
e9b3e4cc91 fix: multiple directory selection on Linux (#45394)
Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2025-01-31 09:38:44 +01:00
trop[bot]
895bc51272 build: fixup concurrent builds on protected branches (#45383)
Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: John Kleinschmidt <jkleinsc@electronjs.org>
2025-01-30 10:36:24 +01:00
Keeley Hammond
ef1ad85082 fix: crash in gin::wrappable::secondweakcallback (#45378)
fix: crash in gin::wrappable::secondweakcallback (#45368)
2025-01-29 15:04:08 -08:00
trop[bot]
e99328a45e docs: reference security guide in ipcRenderer.on docs (#45371)
Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Niklas Wenzel <dev@nikwen.de>
2025-01-29 15:44:07 -05:00
trop[bot]
04f5fe6a1c build: add NSPrefersDisplaySafeAreaCompatibilityMode = false to Info.plist (#45357)
build: add NSPrefersDisplaySafeAreaCompatibilityMode = false to Info.plist

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Milan Burda <milan.burda@gmail.com>
2025-01-29 19:55:03 +01:00
trop[bot]
08b6bb1712 build: use Python311 exe (#45364)
build: yse Python311 exe

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Keeley Hammond <khammond@slack-corp.com>
2025-01-28 20:43:17 -08:00
trop[bot]
813efbcdf7 docs: fix broken code in drag and drop example (#45336)
Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Niklas Wenzel <dev@nikwen.de>
2025-01-27 13:05:42 -05:00
trop[bot]
d34fa2e301 docs: Add note about directly exposing Electron APIs in preload (#45324)
* docs: Add note about directly exposing Electron APIs in preload

Co-authored-by: Felix Rieseberg <fr@makenotion.com>

* Implement feedback

Co-authored-by: Felix Rieseberg <fr@makenotion.com>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Felix Rieseberg <fr@makenotion.com>
2025-01-23 16:29:48 -08:00
trop[bot]
724744af16 chore: better logging if Node initialization fails (#45317)
feat: better logging if Node initialization fails

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Charles Kerr <charles@charleskerr.com>
2025-01-23 12:16:18 -06:00
trop[bot]
91bb748eaa refactor: remove InspectableWebContentsViewMac in favor of the Views version (#45238)
* refactor: remove InspectableWebContentsViewMac in favor of the Views version

* cherry-pick: refactor: remove InspectableWebContentsViewMac in favor of the Views version (#41326)

commit e67ab9a93d

Confilcts not resolved, except removal of the files removed
by the original commit.

* resolved conflicts and build issues after cherry-pick

* cherry-picked: fix: add method allowing to disable headless mode in native widget

https://github.com/electron/electron/pull/42996
fixing
https://github.com/electron/electron/issues/42995

* fix: displaying select popup in window created as fullscreen window

`constrainFrameRect:toScreen:` is not being call for windows created
with `fullscreen: true` therefore `headless` mode was not being removed
and `RenderWidgetHostNSViewBridge::DisplayPopupMenu` ignored displaying
popup.

Issue could be fixed by placing additional removal of `headless` mode
in the `toggleFullScreen:`, but `orderWindow:relativeTo:` is called
both for a regular and a fullscreen window, therefore there will be
a single place fixing both cases.

Because `electron::NativeWindowMac` lifetime may be shorter than
`ElectronNSWindow` on which macOS may execute `orderWindow:relativeTo:`
we need to clear `shell_` when `NativeWindow` is being closed.

Fixes #43010.

* fix: Content visibility when using `vibrancy`

We need to put `NSVisualEffectView` before `ViewsCompositorSuperview`
otherwise when using `vibrancy` in `BrowserWindow` `NSVisualEffectView`
will hide content displayed by the compositor.

Fixes #43003
Fixes #42336

In fact main issues reported in these tickets were not present after
cherry-picking original refactor switching to `views::WebView`, so
text could be selected and click event was properly generated. However
both issues testcases were using `vibrancy` and actual content was
invisible, because it was covered by the `NSVisualEffectView`.

* fix: EXCEPTION_ACCESS_VIOLATION crash on BrowserWindow.destroy()

Restored postponed deletion of the `NativeWindow`.

Restoration caused `DCHECK(new_parent_ui_layer->GetCompositor());` failure
in `BrowserCompositorMac::SetParentUiLayer` after the spec test:
`chrome extensions chrome.webRequest does not take precedence over Electron webRequest - http`
with stack:
```
7   Electron Framework 0x000000011fe07830 content::BrowserCompositorMac::SetParentUiLayer(ui::Layer*) + 628
8   Electron Framework 0x000000011fe0c154 content::RenderWidgetHostViewMac::SetParentUiLayer(ui::Layer*) + 220
9   Electron Framework 0x000000011fe226a8 content::WebContentsViewMac::CreateViewForWidget(content::RenderWidgetHost*) + 600
10  Electron Framework 0x000000011fd37e4c content::WebContentsImpl::CreateRenderWidgetHostViewForRenderManager(content::RenderViewHost*) + 164
11  Electron Framework 0x000000011fb32278 content::RenderFrameHostManager::CreateSpeculativeRenderFrame(content::SiteInstanceImpl*, bool, scoped_refptr<content::BrowsingContextState> const&) + 816
12  Electron Framework 0x000000011fb2ab8c content::RenderFrameHostManager::CreateSpeculativeRenderFrameHost(content::SiteInstanceImpl*, content::SiteInstanceImpl*, bool) + 1308
13  Electron Framework 0x000000011fb28598 content::RenderFrameHostManager::GetFrameHostForNavigation(content::NavigationRequest*, content::BrowsingContextGroupSwap*, std::__Cr::basic_string<char, std::__Cr::char_traits<char>, std::__Cr::allocator<char>>*) + 1796
14  Electron Framework 0x000000011fa78660 content::NavigationRequest::SelectFrameHostForOnRequestFailedInternal(bool, bool, std::__Cr::optional<std::__Cr::basic_string<char, std::__Cr::char_traits<char>, std::__Cr::allocator<char>>> const&) + 280
15  Electron Framework 0x000000011fa6a994 content::NavigationRequest::OnRequestFailedInternal(network::URLLoaderCompletionStatus const&, bool, std::__Cr::optional<std::__Cr::basic_string<char, std::__Cr::char_traits<char>, std::__Cr::allocator<char>>> const&, bo
+ 1008
16  Electron Framework 0x000000011fa7772c content::NavigationRequest::OnRequestFailed(network::URLLoaderCompletionStatus const&) + 72
17  Electron Framework 0x000000011f8554ac content::NavigationURLLoaderImpl::NotifyRequestFailed(network::URLLoaderCompletionStatus const&) + 248
```
This was probably the reason of removing `NativeWindow` immediately
in order to cleanup `views_host_` in `WebContentsViewMac` to prevent
using layer without compositor in `WebContentsViewMac::CreateViewForWidget`.

`[ElectronNSWindowDelegate windowWillClose:]` is deleting window host
and the compositor used by the `NativeWindow` therefore detach `NativeWindow`
contents from parent. This will clear `views_host_` and prevent failing
mentioned `DCHECK`.

Fixes #42975

* chore: Applied review suggestions

Co-authored-by: Michał Pichliński <michal.pichlinski@here.io>

* refactor: directly cleanup shell

Co-authored-by: Samuel Maddock <smaddock@slack-corp.com>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Michał Pichliński <michal.pichlinski@here.io>
Co-authored-by: Samuel Maddock <smaddock@slack-corp.com>
2025-01-23 11:54:15 +01:00
trop[bot]
d77c2d75ed fix: potential crash in chrome.tabs.update() (#45302)
fix: potential crash in chrome.tabs.update()

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2025-01-23 11:53:40 +01:00
trop[bot]
f4c3eb4391 refactor: in StopTracing(), use string literals instead of optional<string> (#45292)
refactor: simplify StopTracing() a little by using a string_view instead of an optional<string>

We have compile-time string literals that we're passing to a method
that takes a string_view argument, so we don't need all this extra
optional<string> scaffolding

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Charles Kerr <charles@charleskerr.com>
2025-01-22 09:43:38 -06:00
trop[bot]
9aca9e9fb6 docs: add DocCardList component for index doc (#45296)
Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Erick Zhao <ezhao@slack-corp.com>
2025-01-22 10:53:19 +01:00
trop[bot]
e0fa647601 refactor: simplify ParseUserScript() (#45288)
refactor: simplify ParseUserScript()

local variable user_script no longer needed after #43205

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Charles Kerr <charles@charleskerr.com>
2025-01-22 10:11:16 +01:00
trop[bot]
6b0fd02c0a fix: webContents.print() with OOP printing (#45285)
* fix: webContents.print() with OOP printing

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>

* Update patches/chromium/printing.patch

Co-authored-by: Robo <hop2deep@gmail.com>

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2025-01-21 15:13:29 -05:00
trop[bot]
1017ac821f chore: align clipboard blink::web_pref::WebPreferences with upstream (#45280)
chore: align clipboard blink::web_pref::WebPreferences with upstream

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2025-01-21 09:39:04 -08:00
trop[bot]
91b53b633a fix: getAsFileSystemHandle failure when drag-dropping two directories (#45256)
fix: drag-dropping two directories

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2025-01-20 14:01:15 -06:00
trop[bot]
8da9572592 fix: two possible FSA crashes (#45261)
* 5786874: Change Observer: Fix crash when navigating to new page

https://chromium-review.googlesource.com/c/chromium/src/+/5786874

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>

* 5794141: Change Observer: Fix Get*PermissionGrant crash

https://chromium-review.googlesource.com/c/chromium/src/+/5794141

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2025-01-20 18:20:55 +01:00
trop[bot]
291bbff5d8 build: fix clang-format duplicate message (#45265)
Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2025-01-20 18:20:47 +01:00
trop[bot]
d704a3fc5b fix: page scaling in silent mode printing (#45262)
Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2025-01-20 10:37:54 -06:00
trop[bot]
fb70b81ee6 fix: broken OOP window.print() on macOS/Linux (#45259)
fix: broken OOP printing on macOS/Linux

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2025-01-20 10:37:35 -06:00
trop[bot]
97fa059e1f docs: remove quickstart (#45209)
Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Anny Yang <yangannyx@gmail.com>
2025-01-17 10:36:58 +01:00
Felix Rieseberg
8a64cdc0b1 docs: Why Electron? (#45191) (#45222)
* docs: Why Electron?

* Apply suggestions from code review




* Update docs/why-electron.md

---------

Co-authored-by: Sam Maddock <samuel.maddock@gmail.com>
Co-authored-by: Erick Zhao <erick@hotmail.ca>
2025-01-16 14:06:57 -05:00
95 changed files with 2816 additions and 1072 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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 }}

View File

@@ -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'

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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:

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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)

View File

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

View File

@@ -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', () => {

View File

@@ -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

View 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.

View File

@@ -56,6 +56,17 @@ Returns:
Emitted when a service worker has been registered. Can occur after a call to [`navigator.serviceWorker.register('/sw.js')`](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register) successfully resolves or when a Chrome extension is loaded.
#### Event: 'running-status-changed' _Experimental_
Returns:
* `details` Event\<\>
* `versionId` number - ID of the updated service worker version
* `runningStatus` string - Running status.
Possible values include `starting`, `running`, `stopping`, or `stopped`.
Emitted when a service worker's running status has changed.
### Instance Methods
The following methods are available on instances of `ServiceWorkers`:
@@ -64,10 +75,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)
}
}
})
```

View File

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

View File

@@ -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.

View 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.

View File

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

View File

@@ -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

View File

@@ -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)
})
```

View File

@@ -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

View File

@@ -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",

View File

@@ -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",

View File

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

View File

@@ -0,0 +1,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;

View File

@@ -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,

View File

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

View File

@@ -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) {

View File

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

View File

@@ -1,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);

View File

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

View File

@@ -0,0 +1,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);
}
}
}

View File

@@ -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

View File

@@ -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;

View File

@@ -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);

View File

@@ -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

View File

@@ -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(

View File

@@ -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);

View File

@@ -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

View File

@@ -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

View File

@@ -236,6 +236,10 @@ const char* Notification::GetTypeName() {
return GetClassName();
}
void Notification::WillBeDestroyed() {
ClearWeak();
}
} // namespace electron::api
namespace {

View File

@@ -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;

View File

@@ -13,11 +13,18 @@
#include "gin/data_object_builder.h"
#include "gin/handle.h"
#include "gin/object_template_builder.h"
#include "shell/browser/api/electron_api_service_worker_main.h"
#include "shell/browser/electron_browser_context.h"
#include "shell/browser/javascript_environment.h"
#include "shell/common/gin_converters/gurl_converter.h"
#include "shell/common/gin_converters/service_worker_converter.h"
#include "shell/common/gin_converters/value_converter.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/promise.h"
#include "shell/common/node_util.h"
using ServiceWorkerStatus =
content::ServiceWorkerRunningInfo::ServiceWorkerVersionStatus;
namespace electron::api {
@@ -72,8 +79,8 @@ gin::WrapperInfo ServiceWorkerContext::kWrapperInfo = {gin::kEmbedderNativeGin};
ServiceWorkerContext::ServiceWorkerContext(
v8::Isolate* isolate,
ElectronBrowserContext* browser_context) {
service_worker_context_ =
browser_context->GetDefaultStoragePartition()->GetServiceWorkerContext();
storage_partition_ = browser_context->GetDefaultStoragePartition();
service_worker_context_ = storage_partition_->GetServiceWorkerContext();
service_worker_context_->AddObserver(this);
}
@@ -81,6 +88,23 @@ ServiceWorkerContext::~ServiceWorkerContext() {
service_worker_context_->RemoveObserver(this);
}
void ServiceWorkerContext::OnRunningStatusChanged(
int64_t version_id,
blink::EmbeddedWorkerStatus running_status) {
ServiceWorkerMain* worker =
ServiceWorkerMain::FromVersionID(version_id, storage_partition_);
if (worker)
worker->OnRunningStatusChanged(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() {

View File

@@ -10,18 +10,30 @@
#include "content/public/browser/service_worker_context_observer.h"
#include "gin/wrappable.h"
#include "shell/browser/event_emitter_mixin.h"
#include "third_party/blink/public/common/service_worker/embedded_worker_status.h"
namespace content {
class StoragePartition;
}
namespace gin {
template <typename T>
class Handle;
} // namespace gin
namespace gin_helper {
template <typename T>
class Promise;
} // namespace gin_helper
namespace electron {
class ElectronBrowserContext;
namespace api {
class ServiceWorkerMain;
class ServiceWorkerContext final
: public gin::Wrappable<ServiceWorkerContext>,
public gin_helper::EventEmitterMixin<ServiceWorkerContext>,
@@ -32,14 +44,39 @@ class ServiceWorkerContext final
ElectronBrowserContext* browser_context);
v8::Local<v8::Value> GetAllRunningWorkerInfo(v8::Isolate* isolate);
v8::Local<v8::Value> GetWorkerInfoFromID(gin_helper::ErrorThrower thrower,
int64_t version_id);
v8::Local<v8::Value> GetInfoFromVersionID(gin_helper::ErrorThrower thrower,
int64_t version_id);
v8::Local<v8::Value> GetFromVersionID(gin_helper::ErrorThrower thrower,
int64_t version_id);
v8::Local<v8::Value> GetWorkerFromVersionID(v8::Isolate* isolate,
int64_t version_id);
gin::Handle<ServiceWorkerMain> GetWorkerFromVersionIDIfExists(
v8::Isolate* isolate,
int64_t version_id);
v8::Local<v8::Promise> StartWorkerForScope(v8::Isolate* isolate, GURL scope);
void DidStartWorkerForScope(
std::shared_ptr<gin_helper::Promise<v8::Local<v8::Value>>> shared_promise,
int64_t version_id,
int process_id,
int thread_id);
void DidFailToStartWorkerForScope(
std::shared_ptr<gin_helper::Promise<v8::Local<v8::Value>>> shared_promise,
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};
};

View 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)

View 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_

View File

@@ -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 {

View File

@@ -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();

View File

@@ -431,6 +431,10 @@ const char* Tray::GetTypeName() {
return GetClassName();
}
void Tray::WillBeDestroyed() {
ClearWeak();
}
} // namespace electron::api
namespace {

View File

@@ -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;

View File

@@ -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());

View File

@@ -40,6 +40,7 @@
#include "shell/browser/event_emitter_mixin.h"
#include "shell/browser/extended_web_contents_observer.h"
#include "shell/browser/osr/osr_paint_event.h"
#include "shell/browser/preload_script.h"
#include "shell/browser/ui/inspectable_web_contents_delegate.h"
#include "shell/browser/ui/inspectable_web_contents_view_delegate.h"
#include "shell/common/gin_helper/cleaned_up_at_exit.h"
@@ -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;

View File

@@ -306,6 +306,10 @@ const char* MessagePort::GetTypeName() {
return "MessagePort";
}
void MessagePort::WillBeDestroyed() {
ClearWeak();
}
} // namespace electron
namespace {

View File

@@ -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();

View File

@@ -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) {

View File

@@ -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();

View File

@@ -0,0 +1,104 @@
// Copyright (c) 2025 Salesforce, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_BROWSER_PRELOAD_SCRIPT_H_
#define ELECTRON_SHELL_BROWSER_PRELOAD_SCRIPT_H_
#include <string_view>
#include "base/containers/fixed_flat_map.h"
#include "base/files/file_path.h"
#include "base/uuid.h"
#include "gin/converter.h"
#include "shell/common/gin_converters/file_path_converter.h"
#include "shell/common/gin_helper/dictionary.h"
namespace electron {
struct PreloadScript {
enum class ScriptType { kWebFrame, kServiceWorker };
std::string id;
ScriptType script_type;
base::FilePath file_path;
// If set, use the deprecated validation behavior of Session.setPreloads
bool deprecated = false;
};
} // namespace electron
namespace gin {
using electron::PreloadScript;
template <>
struct Converter<PreloadScript::ScriptType> {
static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
const PreloadScript::ScriptType& in) {
using Val = PreloadScript::ScriptType;
static constexpr auto Lookup =
base::MakeFixedFlatMap<Val, std::string_view>({
{Val::kWebFrame, "frame"},
{Val::kServiceWorker, "service-worker"},
});
return StringToV8(isolate, Lookup.at(in));
}
static bool FromV8(v8::Isolate* isolate,
v8::Local<v8::Value> val,
PreloadScript::ScriptType* out) {
using Val = PreloadScript::ScriptType;
static constexpr auto Lookup =
base::MakeFixedFlatMap<std::string_view, Val>({
{"frame", Val::kWebFrame},
{"service-worker", Val::kServiceWorker},
});
return FromV8WithLookup(isolate, val, Lookup, out);
}
};
template <>
struct Converter<PreloadScript> {
static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
const PreloadScript& script) {
gin::Dictionary dict(isolate, v8::Object::New(isolate));
dict.Set("filePath", script.file_path.AsUTF8Unsafe());
dict.Set("id", script.id);
dict.Set("type", script.script_type);
return ConvertToV8(isolate, dict).As<v8::Object>();
}
static bool FromV8(v8::Isolate* isolate,
v8::Local<v8::Value> val,
PreloadScript* out) {
gin_helper::Dictionary options;
if (!ConvertFromV8(isolate, val, &options))
return false;
if (PreloadScript::ScriptType script_type;
options.Get("type", &script_type)) {
out->script_type = script_type;
} else {
return false;
}
if (base::FilePath file_path; options.Get("filePath", &file_path)) {
out->file_path = file_path;
} else {
return false;
}
if (std::string id; options.Get("id", &id)) {
out->id = id;
} else {
out->id = base::Uuid::GenerateRandomV4().AsLowercaseString();
}
if (bool deprecated; options.Get("_deprecated", &deprecated)) {
out->deprecated = deprecated;
}
return true;
}
};
} // namespace gin
#endif // ELECTRON_SHELL_BROWSER_PRELOAD_SCRIPT_H_

View File

@@ -64,5 +64,7 @@
<string>${DEFAULT_APP_ASAR_HEADER_SHA}</string>
</dict>
</dict>
<key>NSPrefersDisplaySafeAreaCompatibilityMode</key>
<false/>
</dict>
</plist>

View File

@@ -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

View File

@@ -9,6 +9,7 @@
#include "base/files/file_path.h"
#include "base/supports_user_data.h"
#include "shell/browser/preload_script.h"
namespace content {
class BrowserContext;
@@ -20,17 +21,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

View File

@@ -819,4 +819,8 @@ const char* SimpleURLLoaderWrapper::GetTypeName() {
return "SimpleURLLoaderWrapper";
}
void SimpleURLLoaderWrapper::WillBeDestroyed() {
ClearWeak();
}
} // namespace electron::api

View File

@@ -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,

View File

@@ -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";

View File

@@ -0,0 +1,25 @@
// Copyright (c) 2025 Salesforce, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/common/gin_converters/service_worker_converter.h"
#include "base/containers/fixed_flat_map.h"
namespace gin {
// static
v8::Local<v8::Value> Converter<blink::EmbeddedWorkerStatus>::ToV8(
v8::Isolate* isolate,
const blink::EmbeddedWorkerStatus& val) {
static constexpr auto Lookup =
base::MakeFixedFlatMap<blink::EmbeddedWorkerStatus, std::string_view>({
{blink::EmbeddedWorkerStatus::kStarting, "starting"},
{blink::EmbeddedWorkerStatus::kRunning, "running"},
{blink::EmbeddedWorkerStatus::kStopping, "stopping"},
{blink::EmbeddedWorkerStatus::kStopped, "stopped"},
});
return StringToV8(isolate, Lookup.at(val));
}
} // namespace gin

View File

@@ -0,0 +1,21 @@
// Copyright (c) 2025 Salesforce, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_COMMON_GIN_CONVERTERS_SERVICE_WORKER_CONVERTER_H_
#define ELECTRON_SHELL_COMMON_GIN_CONVERTERS_SERVICE_WORKER_CONVERTER_H_
#include "gin/converter.h"
#include "third_party/blink/public/common/service_worker/embedded_worker_status.h"
namespace gin {
template <>
struct Converter<blink::EmbeddedWorkerStatus> {
static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
const blink::EmbeddedWorkerStatus& val);
};
} // namespace gin
#endif // ELECTRON_SHELL_COMMON_GIN_CONVERTERS_SERVICE_WORKER_CONVERTER_H_

View File

@@ -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;
}
}

View File

@@ -19,6 +19,8 @@ class CleanedUpAtExit {
CleanedUpAtExit();
virtual ~CleanedUpAtExit();
virtual void WillBeDestroyed();
static void DoCleanup();
};

View File

@@ -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));
}

View File

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

View File

@@ -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

View File

@@ -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(

View File

@@ -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

View File

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

View File

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

View File

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

View File

@@ -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'));

View File

@@ -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'

View File

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

View File

@@ -0,0 +1,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}/`);
});
});
});

View File

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

View File

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

View File

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

View File

@@ -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');
};

View File

@@ -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

View File

@@ -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 };

View File

@@ -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 {