Compare commits

...

20 Commits

Author SHA1 Message Date
trop[bot]
b38c88664e fix: remove vestigial MachServices from ShipIt launchd job (#51111)
Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Keeley Hammond <khammond@slack-corp.com>
2026-04-17 00:20:13 +00:00
trop[bot]
687bd0a1f0 fix: fix types in devtools console for release (#51108)
Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Keeley Hammond <khammond@slack-corp.com>
2026-04-16 15:15:58 -07:00
trop[bot]
9d194e28ab ci: build a patched siso for Windows builds (#51093)
* ci: build a patched siso for Windows builds

The Windows Chromium builds intermittently fail during manifest load
with 'The parameter is incorrect.' (ERROR_INVALID_PARAMETER) out of
bindflt.sys. Root cause is a handle-relative NtCreateFile race in
siso/toolsupport/ninjautil/file_parser.go, which opens each subninja
twice — once in the outer goroutine and once more per chunk for
ReadAt. (*os.File).ReadAt is documented as safe for concurrent use,
so the extra open is redundant and removing it both halves the
CreateFileW calls per subninja and sidesteps the race.

Add a new build-siso-windows job on ubuntu-latest (runs in parallel
with checkout-windows) that:

- reads chromium_version from DEPS and pulls the matching siso_version
  SHA from the Chromium mirror's DEPS at that ref
- shallow-clones chromium.googlesource.com/build at that SHA
- applies the in-tree patches under .github/siso-patches/ via git am
- cross-compiles siso.exe for windows/amd64
- caches the binary keyed on siso SHA + sha256 of the patches, so
  subsequent runs hit the cache and skip the clone/patch/build steps
- uploads the result as a siso-windows-amd64 artifact

The Windows build jobs now depend on build-siso-windows, download the
artifact into $RUNNER_TEMP/siso, and export SISO_PATH, which
depot_tools/siso.py already honors. Mirrored into windows-publish.yml
and the regenerated pipeline-segment-electron-publish.yml so release
builds pick it up too.

Notes: none

Co-authored-by: Sam Attard <sattard@anthropic.com>

* ci: extract siso build into a reusable workflow segment

Move the build-siso-windows job body into
pipeline-segment-build-siso-windows.yml and call it from both build.yml
and windows-publish.yml via workflow_call. Also pin actions/cache to
v5.0.5 and add version comments next to the action SHAs introduced by
this change.

Co-authored-by: Sam Attard <sattard@anthropic.com>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Sam Attard <sattard@anthropic.com>
2026-04-16 15:11:57 -07:00
trop[bot]
b6de8acc8a chore: add Node.js skill to settings (#51106)
Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2026-04-16 15:10:59 -07:00
trop[bot]
b51e62e560 test: add tab source ID tests for media handler (#51095)
* test: add getMediaSourceId tab source coverage

Co-authored-by: Charles Kerr <charles@charleskerr.com>

* chore: move captureWithTabSourceId() to a shared helper

Co-authored-by: Charles Kerr <charles@charleskerr.com>

* test: improve "webContents module getMediaSourceId()" testing

Co-authored-by: Charles Kerr <charles@charleskerr.com>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Charles Kerr <charles@charleskerr.com>
2026-04-16 15:25:33 -04:00
trop[bot]
f1958d838c fix: show 'Electron Isolated Context' in Dev Tools (#51079)
Because of a bug after the [upstream refactor][0] Dev Tools stopped
showing 'Electron Isolated Context' in the execution context selector.
'Electron Isolated Context' runs with origin set to `file://`. Since
domain name is empty for the origin the respective UI item in the
context selector is created with an empty `subtitle`. However, with the
upstream change items with either of `title` or `subtitle` are omitted
from rendering.

Here we float an [in-review patch][1] until it is fixed upstream.

[0]: dbb61cf4b2
[1]: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/7761316

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Fedor Indutny <indutny@signal.org>
2026-04-16 15:22:49 -04:00
trop[bot]
8b2dba3726 fix: prevent uaf when destroying guest WebContents during event emission (#51082)
fix: prevent use-after-free when destroying guest WebContents during event emission

Multiple event emission sites in WebContents destroy the underlying C++
object via a JavaScript event handler calling webContents.destroy(), then
continue to dereference the freed `this` pointer. This is exploitable
through <webview> guest WebContents because Destroy() calls `delete this`
synchronously for guests, unlike non-guests which safely defer deletion.

The fix has two layers:

1. A new `is_emitting_event_` flag is checked in Destroy() — when true,
   guest deletion is deferred to a posted task instead of executing
   synchronously. This is separate from `is_safe_to_delete_` (which
   gates LoadURL re-entrancy) to avoid rejecting legitimate loadURL
   calls from event handlers.

2. AutoReset<bool> guards on `is_emitting_event_` are added to
   CloseContents, RenderViewDeleted, DidFinishNavigation, and
   SetContentsBounds, preventing synchronous destruction while their
   Emit() calls are on the stack.

Destroy() now requires both `is_safe_to_delete_` (navigation re-entrancy)
and `!is_emitting_event_` (event emission) to allow synchronous guest
deletion. The existing AutoReset guards on `is_safe_to_delete_` in
DidStartNavigation, DidRedirectNavigation, and ReadyToCommitNavigation
are also now effective for guests.

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2026-04-16 13:01:31 -04:00
trop[bot]
4ac50292d5 fix: use CreateDataProperty when copying objects across contextBridge (#51086)
Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Sam Attard <sattard@anthropic.com>
2026-04-16 12:42:49 -04:00
trop[bot]
81e76165ae fix: allow PDF viewer to show save file picker (#51072)
The PDF viewer's "save with changes" feature uses
`window.showSaveFilePicker()`, but the PDF extension runs in a
cross-origin iframe (chrome-extension:// inside the app's origin).
Chromium's File System Access API blocks cross-origin subframes from
showing file pickers unless the embedder explicitly allows them via
`ContentClient::IsFilePickerAllowedForCrossOriginSubframe()`.

Chrome overrides this in `ChromeContentClient` to allowlist the PDF
extension origin, but Electron never did — so the picker was always
blocked with a SecurityError.

This adds the same override to `ElectronContentClient`, allowing the
built-in PDF extension origin to bypass the cross-origin check.

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2026-04-15 21:55:17 -05:00
trop[bot]
acf615229d build: fail gha-done check when required job fails (#51067)
fix: fail gha-done when any required job failed

Previously, the `gha-done` gate job used an `if:` expression that
evaluated to false whenever any needed job reported a failure, which
caused the job to be *skipped* rather than *failed*. GitHub branch
protection treats skipped required checks as non-blocking, so a PR
could be marked mergeable even though one of its test jobs had failed.

Keep the job always running and move the failure check into a step
that explicitly exits 1 when any dependency failed or was cancelled,
so the "GitHub Actions Completed" required check actually blocks the
merge in that case.

Notes: none

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Samuel Attard <samuel.r.attard@gmail.com>
2026-04-15 18:02:13 +02:00
trop[bot]
d5cea60ac7 chore: remove unused parts of chore_provide_iswebcontentscreationoverridden_with_full_params.patch (#51043)
chore: remove dead patches from chore_provide_iswebcontentscreationoverridden_with_full_params.patch

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Charles Kerr <charles@charleskerr.com>
2026-04-15 11:00:49 +02:00
Samuel Attard
60ec7cd0fb build: authenticate sudowoodo /token exchange via Actions OIDC (42-x-y) (#51052)
build: authenticate sudowoodo /token exchange via Actions OIDC
2026-04-14 20:44:06 -07:00
John Kleinschmidt
3d7d676bee test: fixup autoupdater tests failures (#51050) 2026-04-14 19:54:44 -05:00
trop[bot]
c5b0ee8a9b docs: mention pre-release installation (#51044)
* docs: pre-release installation

Co-authored-by: Erick Zhao <erick@hotmail.ca>

* Update installation.md

Co-authored-by: Niklas Wenzel <dev@nikwen.de>

Co-authored-by: Erick Zhao <erick@hotmail.ca>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Erick Zhao <erick@hotmail.ca>
2026-04-14 11:53:13 -07:00
trop[bot]
3ea2c9c760 fix: crash when closing devtools after focus (#51036)
Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2026-04-14 09:23:33 -07:00
trop[bot]
067fe3d1f1 ci: don't login to RBE for clang-tidy and gn-check (#51038)
* ci: don't login to RBE for clang-tidy

Co-authored-by: John Kleinschmidt <kleinschmidtorama@gmail.com>

* ci: don't login to RBE for gn check

Co-authored-by: John Kleinschmidt <kleinschmidtorama@gmail.com>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: John Kleinschmidt <kleinschmidtorama@gmail.com>
2026-04-14 16:05:52 +02:00
trop[bot]
75a7ebc7c0 refactor: migrate api::Extensions to cppgc (#50956)
* refactor: migrate api::Extensions to cppgc

Co-authored-by: Charles Kerr <charles@charleskerr.com>

* chore: update patch indices

Co-authored-by: Charles Kerr <charles@charleskerr.com>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Charles Kerr <charles@charleskerr.com>
2026-04-14 01:04:24 -05:00
trop[bot]
64055f27e7 feat: add id, groupId, and groupTitle support for Windows notifications (#50895)
* feat: allow to set id and groupId

Co-authored-by: Jan Hannemann <jan.hannemann@outlook.com>

* feat: use Id's without hash but check length

Co-authored-by: Jan Hannemann <jan.hannemann@outlook.com>

* feat: adds visual grouping via groupTitle

Co-authored-by: Jan Hannemann <jan.hannemann@outlook.com>

* test: tests added for id, groupId and groupTitle

Co-authored-by: Jan Hannemann <jan.hannemann@outlook.com>

* fix: unused vars on Mac and Linux

Co-authored-by: Jan Hannemann <jan.hannemann@outlook.com>

* fix: remove redundant parameter

Co-authored-by: Jan Hannemann <jan.hannemann@outlook.com>

* fix: add doc links for id and group

Co-authored-by: Jan Hannemann <jan.hannemann@outlook.com>

* fix: throw if groupId is missing

Co-authored-by: Jan Hannemann <jan.hannemann@outlook.com>

* fix: test

Co-authored-by: Jan Hannemann <jan.hannemann@outlook.com>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Jan Hannemann <jan.hannemann@outlook.com>
2026-04-13 16:01:01 -07:00
trop[bot]
09156151c4 fix: dangling raw_ptr api::Protocol::protocol_registry_ (#50951)
Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Charles Kerr <charles@charleskerr.com>
2026-04-13 16:18:25 -05:00
trop[bot]
b07503d468 ci: capture fatal errors in clang problem matcher (#50998)
Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: David Sanders <dsanders11@ucsbalum.com>
2026-04-13 14:22:59 -05:00
45 changed files with 921 additions and 390 deletions

View File

@@ -11,6 +11,7 @@
"Bash(e patches:*)",
"Bash(e sync:*)",
"Skill(electron-chromium-upgrade)",
"Skill(electron-node-upgrade)",
"Read(*)",
"Bash(echo:*)",
"Bash(e build:*)",

View File

@@ -5,7 +5,7 @@
"fromPath": "src/out/Default/args.gn",
"pattern": [
{
"regexp": "^(.+)[(:](\\d+)[:,](\\d+)\\)?:\\s+(warning|error):\\s+(.*)$",
"regexp": "^(.+)[(:](\\d+)[:,](\\d+)\\)?:\\s+(warning|fatal error|error):\\s+(.*)$",
"file": 1,
"line": 2,
"column": 3,

View File

@@ -0,0 +1,47 @@
From 85b561ea4dbc76ba98af020b970f3aa6b20fdb9e Mon Sep 17 00:00:00 2001
From: Samuel Attard <sam@electronjs.org>
Date: Wed, 8 Apr 2026 23:24:15 -0700
Subject: [PATCH] siso: reuse the outer *os.File for chunked ReadAt in
fileParser.readFile
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The per-chunk goroutine currently re-opens fname to get its own handle
for ReadAt. (*os.File).ReadAt is documented as safe for concurrent
calls on the same File (on Windows it is ReadFile with an OVERLAPPED
offset, so there is no shared seek state), so the extra open is
redundant — the goroutines can share the outer f.
Besides halving the CreateFileW calls per subninja, this avoids an
intermittent 'The parameter is incorrect.' (ERROR_INVALID_PARAMETER)
from bindflt.sys when out/ is a mapped directory inside a Windows
container: bindflt's handle-relative NtCreateFile path races when a
second relative open arrives while the first handle to the same target
is still being set up. Absolute paths and single opens do not trigger
it; see microsoft/Windows-Containers#<tbd>.
---
siso/toolsupport/ninjautil/file_parser.go | 7 -------
1 file changed, 7 deletions(-)
diff --git a/siso/toolsupport/ninjautil/file_parser.go b/siso/toolsupport/ninjautil/file_parser.go
index 8c18d084..63116662 100644
--- a/siso/toolsupport/ninjautil/file_parser.go
+++ b/siso/toolsupport/ninjautil/file_parser.go
@@ -111,13 +111,6 @@ func (p *fileParser) readFile(ctx context.Context, fname string) ([]byte, error)
eg.Go(func() error {
p.sema <- struct{}{}
defer func() { <-p.sema }()
- f, err := os.Open(fname)
- if err != nil {
- return err
- }
- defer func() {
- _ = f.Close()
- }()
for len(chunkBuf) > 0 {
n, err := f.ReadAt(chunkBuf, pos)
if err != nil {
--
2.53.0

View File

@@ -200,6 +200,15 @@ jobs:
generate-sas-token: 'true'
target-platform: win
# Build a patched siso binary for Windows CI in parallel with checkout-windows.
# The Windows build jobs download the resulting artifact and use it via SISO_PATH.
build-siso-windows:
needs: setup
if: ${{ needs.setup.outputs.src == 'true' && !inputs.skip-windows }}
uses: ./.github/workflows/pipeline-segment-build-siso-windows.yml
permissions:
contents: read
# GN Check Jobs
macos-gn-check:
uses: ./.github/workflows/pipeline-segment-electron-gn-check.yml
@@ -384,7 +393,7 @@ jobs:
issues: read
pull-requests: read
uses: ./.github/workflows/pipeline-electron-build-and-test.yml
needs: checkout-windows
needs: [checkout-windows, build-siso-windows]
if: ${{ needs.setup.outputs.src == 'true' && !inputs.skip-windows }}
with:
build-runs-on: electron-arc-centralus-windows-amd64-16core
@@ -403,7 +412,7 @@ jobs:
issues: read
pull-requests: read
uses: ./.github/workflows/pipeline-electron-build-and-test.yml
needs: checkout-windows
needs: [checkout-windows, build-siso-windows]
if: ${{ needs.setup.outputs.src == 'true' && !inputs.skip-windows }}
with:
build-runs-on: electron-arc-centralus-windows-amd64-16core
@@ -422,7 +431,7 @@ jobs:
issues: read
pull-requests: read
uses: ./.github/workflows/pipeline-electron-build-and-test.yml
needs: checkout-windows
needs: [checkout-windows, build-siso-windows]
if: ${{ needs.setup.outputs.src == 'true' && !inputs.skip-windows }}
with:
build-runs-on: electron-arc-centralus-windows-amd64-16core
@@ -440,9 +449,12 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read
needs: [docs-only, macos-x64, macos-arm64, linux-x64, linux-x64-asan, linux-arm, linux-arm64, windows-x64, windows-x86, windows-arm64]
if: always() && github.repository == 'electron/electron' && !contains(needs.*.result, 'failure')
needs: [docs-only, macos-x64, macos-arm64, linux-x64, linux-x64-asan, linux-arm, linux-arm64, build-siso-windows, windows-x64, windows-x86, windows-arm64]
if: always() && github.repository == 'electron/electron'
steps:
- name: Fail if any needed job failed or was cancelled
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
run: exit 1
- name: GitHub Actions Jobs Done
run: |
echo "All GitHub Actions Jobs are done"

View File

@@ -0,0 +1,98 @@
name: Pipeline Segment - Build Siso (Windows)
# Builds a patched siso binary for Windows CI. Reads the siso revision from
# the Chromium DEPS file at the pinned chromium_version, shallow-clones
# chromium.googlesource.com/build at that revision, applies the patches under
# .github/siso-patches/, cross-compiles siso.exe for windows/amd64, and
# publishes it as the `siso-windows-amd64` artifact. The Windows build jobs
# download it and use it via SISO_PATH. The built binary is cached keyed on
# the siso revision + sha256 of the patch contents, so subsequent runs just
# restore it.
on:
workflow_call: {}
permissions: {}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout Electron
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 1
ref: ${{ github.event.pull_request.head.sha }}
sparse-checkout: |
DEPS
.github/siso-patches
- name: Resolve siso revision from Chromium DEPS
id: resolve
run: |
set -euo pipefail
CHROMIUM_VERSION=$(python3 -c "import re; print(re.search(r\"'chromium_version':\s*\n\s*'([^']+)'\", open('DEPS').read()).group(1))")
if ! [[ "$CHROMIUM_VERSION" =~ ^[0-9]+(\.[0-9]+){1,3}$ ]]; then
echo "error: unexpected chromium_version format: $CHROMIUM_VERSION" >&2
exit 1
fi
curl -sfL "https://raw.githubusercontent.com/chromium/chromium/${CHROMIUM_VERSION}/DEPS" -o /tmp/chromium-DEPS
SISO_SHA=$(python3 -c "import re; print(re.search(r\"'siso_version':\s*'git_revision:([0-9a-f]+)'\", open('/tmp/chromium-DEPS').read()).group(1))")
if ! [[ "$SISO_SHA" =~ ^[0-9a-f]{40}$ ]]; then
echo "error: unexpected siso_version SHA: $SISO_SHA" >&2
exit 1
fi
PATCHES_HASH=$(find .github/siso-patches -type f -name '*.patch' | sort | xargs sha256sum | sha256sum | awk '{print $1}')
echo "siso-sha=${SISO_SHA}" >> "$GITHUB_OUTPUT"
echo "patches-hash=${PATCHES_HASH}" >> "$GITHUB_OUTPUT"
echo "Chromium ${CHROMIUM_VERSION} pins siso at ${SISO_SHA}"
echo "Patches hash: ${PATCHES_HASH}"
- name: Restore cached siso binary
id: cache-siso
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: siso-out/siso.exe
key: siso-windows-amd64-${{ steps.resolve.outputs.siso-sha }}-${{ steps.resolve.outputs.patches-hash }}
- name: Shallow clone chromium build repo at pinned revision
if: steps.cache-siso.outputs.cache-hit != 'true'
env:
SISO_SHA: ${{ steps.resolve.outputs.siso-sha }}
run: |
set -euo pipefail
mkdir chromium-build
cd chromium-build
git init -q
git remote add origin https://chromium.googlesource.com/build
git -c protocol.version=2 fetch --depth=1 origin "$SISO_SHA"
git checkout --detach FETCH_HEAD
- name: Apply in-tree siso patches
if: steps.cache-siso.outputs.cache-hit != 'true'
run: |
set -euo pipefail
cd chromium-build
git -c user.name=electron-ci -c user.email=ci@electronjs.org \
am --3way "${GITHUB_WORKSPACE}/.github/siso-patches"/*.patch
- name: Set up Go
if: steps.cache-siso.outputs.cache-hit != 'true'
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: chromium-build/siso/go.mod
cache: false
- name: Build siso (windows/amd64)
if: steps.cache-siso.outputs.cache-hit != 'true'
working-directory: chromium-build/siso
env:
CGO_ENABLED: '0'
GOOS: windows
GOARCH: amd64
run: |
mkdir -p "${GITHUB_WORKSPACE}/siso-out"
go build -trimpath -o "${GITHUB_WORKSPACE}/siso-out/siso.exe" .
- name: Upload siso artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: siso-windows-amd64
path: siso-out/siso.exe
if-no-files-found: error
retention-days: 1

View File

@@ -77,7 +77,6 @@ env:
ELECTRON_ARTIFACTS_BLOB_STORAGE: ${{ secrets.ELECTRON_ARTIFACTS_BLOB_STORAGE }}
ELECTRON_RBE_JWT: ${{ secrets.ELECTRON_RBE_JWT }}
SUDOWOODO_EXCHANGE_URL: ${{ secrets.SUDOWOODO_EXCHANGE_URL }}
SUDOWOODO_EXCHANGE_TOKEN: ${{ secrets.SUDOWOODO_EXCHANGE_TOKEN }}
GCLIENT_EXTRA_ARGS: ${{ inputs.target-platform == 'macos' && '--custom-var=checkout_mac=True --custom-var=host_os=mac' || inputs.target-platform == 'win' && '--custom-var=checkout_win=True' || '--custom-var=checkout_arm=True --custom-var=checkout_arm64=True' }}
ELECTRON_OUT_DIR: Default
ACTIONS_STEP_DEBUG: ${{ secrets.ACTIONS_STEP_DEBUG }}
@@ -195,6 +194,22 @@ jobs:
- name: Free up space (macOS)
if: ${{ inputs.target-platform == 'macos' }}
uses: ./src/electron/.github/actions/free-space-macos
- name: Download custom siso binary (Windows)
if: ${{ inputs.target-platform == 'win' }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: siso-windows-amd64
path: ${{ runner.temp }}/siso
- name: Set SISO_PATH (Windows)
if: ${{ inputs.target-platform == 'win' }}
run: |
SISO_BIN="${RUNNER_TEMP}/siso/siso.exe"
if [ ! -f "$SISO_BIN" ]; then
echo "error: expected siso binary at $SISO_BIN" >&2
exit 1
fi
echo "SISO_PATH=$SISO_BIN" >> "$GITHUB_ENV"
echo "Using custom siso binary at $SISO_BIN"
- name: Build Electron
if: ${{ inputs.target-platform != 'macos' || (inputs.target-variant == 'all' || inputs.target-variant == 'darwin') }}
uses: ./src/electron/.github/actions/build-electron

View File

@@ -135,7 +135,7 @@ jobs:
run: echo "::add-matcher::src/electron/.github/problem-matchers/clang.json"
- name: Run Clang-Tidy
run: |
e init -f --root=$(pwd) --out=${ELECTRON_OUT_DIR} testing --target-cpu ${TARGET_ARCH}
e init -f --root=$(pwd) --out=${ELECTRON_OUT_DIR} testing --target-cpu ${TARGET_ARCH} --remote-build none
export GN_EXTRA_ARGS="target_cpu=\"${TARGET_ARCH}\""
if [ "${{ inputs.target-platform }}" = "win" ]; then

View File

@@ -130,7 +130,7 @@ jobs:
run: |
for target_cpu in ${{ inputs.target-archs }}
do
e init -f --root=$(pwd) --out=Default ${{ inputs.gn-build-type }} --import ${{ inputs.gn-build-type }} --target-cpu $target_cpu
e init -f --root=$(pwd) --out=Default ${{ inputs.gn-build-type }} --import ${{ inputs.gn-build-type }} --target-cpu $target_cpu --remote-build none
cd src
export GN_EXTRA_ARGS="target_cpu=\"$target_cpu\""
if [ "${{ inputs.target-platform }}" = "linux" ]; then

View File

@@ -79,7 +79,6 @@ env:
ELECTRON_ARTIFACTS_BLOB_STORAGE: ${{ secrets.ELECTRON_ARTIFACTS_BLOB_STORAGE }}
ELECTRON_RBE_JWT: ${{ secrets.ELECTRON_RBE_JWT }}
SUDOWOODO_EXCHANGE_URL: ${{ secrets.SUDOWOODO_EXCHANGE_URL }}
SUDOWOODO_EXCHANGE_TOKEN: ${{ secrets.SUDOWOODO_EXCHANGE_TOKEN }}
GCLIENT_EXTRA_ARGS: ${{ inputs.target-platform == 'macos' &&
'--custom-var=checkout_mac=True --custom-var=host_os=mac' ||
inputs.target-platform == 'win' && '--custom-var=checkout_win=True' ||
@@ -208,6 +207,22 @@ jobs:
- name: Free up space (macOS)
if: ${{ inputs.target-platform == 'macos' }}
uses: ./src/electron/.github/actions/free-space-macos
- name: Download custom siso binary (Windows)
if: ${{ inputs.target-platform == 'win' }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: siso-windows-amd64
path: ${{ runner.temp }}/siso
- name: Set SISO_PATH (Windows)
if: ${{ inputs.target-platform == 'win' }}
run: |
SISO_BIN="${RUNNER_TEMP}/siso/siso.exe"
if [ ! -f "$SISO_BIN" ]; then
echo "error: expected siso binary at $SISO_BIN" >&2
exit 1
fi
echo "SISO_PATH=$SISO_BIN" >> "$GITHUB_ENV"
echo "Using custom siso binary at $SISO_BIN"
- name: Build Electron
if: ${{ inputs.target-platform != 'macos' || (inputs.target-variant == 'all' ||
inputs.target-variant == 'darwin') }}

View File

@@ -51,6 +51,14 @@ jobs:
generate-sas-token: 'true'
target-platform: win
# Build the patched siso binary in parallel with checkout-windows; the
# publish-*-win jobs consume it via SISO_PATH.
build-siso-windows:
if: github.repository == 'electron/electron'
uses: ./.github/workflows/pipeline-segment-build-siso-windows.yml
permissions:
contents: read
publish-x64-win:
uses: ./.github/workflows/pipeline-segment-electron-publish.yml
permissions:
@@ -58,7 +66,7 @@ jobs:
attestations: write
contents: read
id-token: write
needs: checkout-windows
needs: [checkout-windows, build-siso-windows]
with:
environment: production-release
build-runs-on: electron-arc-centralus-windows-amd64-16core
@@ -77,7 +85,7 @@ jobs:
attestations: write
contents: read
id-token: write
needs: checkout-windows
needs: [checkout-windows, build-siso-windows]
with:
environment: production-release
build-runs-on: electron-arc-centralus-windows-amd64-16core
@@ -96,7 +104,7 @@ jobs:
attestations: write
contents: read
id-token: write
needs: checkout-windows
needs: [checkout-windows, build-siso-windows]
with:
environment: production-release
build-runs-on: electron-arc-centralus-windows-amd64-16core

View File

@@ -79,8 +79,9 @@ app.whenReady().then(() => {
### `new Notification([options])`
* `options` Object (optional)
* `id` string (optional) _macOS_ - A unique identifier for the notification, mapping to `UNNotificationRequest`'s [`identifier`](https://developer.apple.com/documentation/usernotifications/unnotificationrequest/identifier) property. Defaults to a random UUID if not provided or if an empty string is passed. This can be used to remove or update previously delivered notifications.
* `groupId` string (optional) _macOS_ - A string identifier used to visually group notifications together in Notification Center. Maps to `UNNotificationContent`'s [`threadIdentifier`](https://developer.apple.com/documentation/usernotifications/unnotificationcontent/threadidentifier) property.
* `id` string (optional) _macOS_ _Windows_ - A unique identifier for the notification. On macOS, maps to `UNNotificationRequest`'s [`identifier`](https://developer.apple.com/documentation/usernotifications/unnotificationrequest/identifier) property. On Windows, maps to the toast notification's [`Tag`](https://learn.microsoft.com/en-us/uwp/api/windows.ui.notifications.toastnotification.tag) property. Defaults to a random UUID if not provided or if an empty string is passed. This can be used to remove or update previously delivered notifications.
* `groupId` string (optional) _macOS_ _Windows_ - A string identifier used to visually group notifications together in Notification Center / Action Center. On macOS, maps to `UNNotificationContent`'s [`threadIdentifier`](https://developer.apple.com/documentation/usernotifications/unnotificationcontent/threadidentifier) property. On Windows, maps to the toast notification's [`Group`](https://learn.microsoft.com/en-us/uwp/api/windows.ui.notifications.toastnotification.group) property.
* `groupTitle` string (optional) _Windows_ - A title for the notification group header. When both `groupId` and `groupTitle` are specified, Windows will display a header above the notification that groups related notifications together. Maps to the toast notification's [`header`](https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/toast-headers) element.
* `title` string (optional) - A title for the notification, which will be displayed at the top of the notification window when it is shown.
* `subtitle` string (optional) _macOS_ - A subtitle for the notification, which will be displayed below the title.
* `body` string (optional) - The body text of the notification, which will be displayed below the title or subtitle.
@@ -329,13 +330,17 @@ app.whenReady().then(() => {
### Instance Properties
#### `notification.id` _macOS_ _Readonly_
#### `notification.id` _macOS_ _Windows_ _Readonly_
A `string` property representing the unique identifier of the notification. This is set at construction time — either from the `id` option or as a generated UUID if none was provided.
#### `notification.groupId` _macOS_ _Readonly_
#### `notification.groupId` _macOS_ _Windows_ _Readonly_
A `string` property representing the group identifier of the notification. Notifications with the same `groupId` will be visually grouped together in Notification Center.
A `string` property representing the group identifier of the notification. Notifications with the same `groupId` will be visually grouped together in Notification Center (macOS) or Action Center (Windows).
#### `notification.groupTitle` _Windows_ _Readonly_
A `string` property representing the title of the notification group header.
#### `notification.title`

View File

@@ -25,6 +25,27 @@ included in the `electron` package:
npx install-electron --no
```
## Installing prereleases
Electron [distributes experimental releases of future major versions](./electron-timelines.md)
via npm as well.
Nightly builds contain the latest changes from the `main` branch:
```sh
npm install electron-nightly --save-dev
```
Alpha and beta builds contain changes slated for the next major version:
```sh
npm install electron@alpha --save-dev
npm install electron@beta --save-dev
```
> [!TIP]
> For more information on available Electron releases, see the [Release Status dashboard](https://releases.electronjs.org).
## Running Electron ad-hoc
If you're in a pinch and would prefer to not use `npm install` in your local

View File

@@ -8,10 +8,10 @@ electron objects that extend gin::Wrappable and gets
allocated on the cpp heap
diff --git a/gin/public/wrappable_pointer_tags.h b/gin/public/wrappable_pointer_tags.h
index fee622ebde42211de6f702b754cfa38595df5a1c..7bc00b2941cc4aaf0ae02a0db8722f74a0c228d9 100644
index fee622ebde42211de6f702b754cfa38595df5a1c..0f649fa562cef261b127dca7769dd5687a916342 100644
--- a/gin/public/wrappable_pointer_tags.h
+++ b/gin/public/wrappable_pointer_tags.h
@@ -77,7 +77,22 @@ enum WrappablePointerTag : uint16_t {
@@ -77,7 +77,23 @@ enum WrappablePointerTag : uint16_t {
kWebAXObjectProxy, // content::WebAXObjectProxy
kWrappedExceptionHandler, // extensions::WrappedExceptionHandler
kIndigoContext, // indigo::IndigoContext
@@ -20,6 +20,7 @@ index fee622ebde42211de6f702b754cfa38595df5a1c..7bc00b2941cc4aaf0ae02a0db8722f74
+ kElectronDataPipeHolder, // electron::api::DataPipeHolder
+ kElectronDebugger, // electron::api::Debugger
+ kElectronEvent, // gin_helper::internal::Event
+ kElectronExtensions, // electron::api::Extensions
+ kElectronMenu, // electron::api::Menu
+ kElectronNetLog, // electron::api::NetLog
+ kElectronPowerMonitor, // electron::api::PowerMonitor

View File

@@ -6,79 +6,6 @@ Subject: chore: provide IsWebContentsCreationOverridden with full params
Pending upstream patch, this gives us fuller access to the window.open params
so that we will be able to decide whether to cancel it or not.
diff --git a/chrome/browser/media/offscreen_tab.cc b/chrome/browser/media/offscreen_tab.cc
index 047f1258f951f763df2ca0ba355b19d19337826b..9fc7114312212fbe38ddec740b4aebbcd72cb0f8 100644
--- a/chrome/browser/media/offscreen_tab.cc
+++ b/chrome/browser/media/offscreen_tab.cc
@@ -285,8 +285,7 @@ bool OffscreenTab::IsWebContentsCreationOverridden(
content::SiteInstance* source_site_instance,
content::mojom::WindowContainerType window_container_type,
const GURL& opener_url,
- const std::string& frame_name,
- const GURL& target_url) {
+ const content::mojom::CreateNewWindowParams& params) {
// Disallow creating separate WebContentses. The WebContents implementation
// uses this to spawn new windows/tabs, which is also not allowed for
// offscreen tabs.
diff --git a/chrome/browser/media/offscreen_tab.h b/chrome/browser/media/offscreen_tab.h
index 231e3595f218aeebe28d0b13ce6182e7a4d6f4e1..609bd205d1cd0404cab3471765bef8b0e053d061 100644
--- a/chrome/browser/media/offscreen_tab.h
+++ b/chrome/browser/media/offscreen_tab.h
@@ -108,8 +108,7 @@ class OffscreenTab final : public ProfileObserver,
content::SiteInstance* source_site_instance,
content::mojom::WindowContainerType window_container_type,
const GURL& opener_url,
- const std::string& frame_name,
- const GURL& target_url) final;
+ const content::mojom::CreateNewWindowParams& params) override;
void EnterFullscreenModeForTab(
content::RenderFrameHost* requesting_frame,
const blink::mojom::FullscreenOptions& options) final;
diff --git a/chrome/browser/ui/ash/keyboard/chrome_keyboard_web_contents.cc b/chrome/browser/ui/ash/keyboard/chrome_keyboard_web_contents.cc
index 33edb0a90d886dd44956046e03fcc182a0f6bc8e..5b5edd3da3d9f7a248ea3affd195c36bfd64a38e 100644
--- a/chrome/browser/ui/ash/keyboard/chrome_keyboard_web_contents.cc
+++ b/chrome/browser/ui/ash/keyboard/chrome_keyboard_web_contents.cc
@@ -80,8 +80,7 @@ class ChromeKeyboardContentsDelegate : public content::WebContentsDelegate,
content::SiteInstance* source_site_instance,
content::mojom::WindowContainerType window_container_type,
const GURL& opener_url,
- const std::string& frame_name,
- const GURL& target_url) override {
+ const content::mojom::CreateNewWindowParams& params) override {
return true;
}
diff --git a/chrome/browser/ui/ash/web_view/ash_web_view_impl.cc b/chrome/browser/ui/ash/web_view/ash_web_view_impl.cc
index e4e42249c476ccae58f0ba42e7dbae299f1e36bd..670c30ed4b7f1a07eb4b8abaa95e5a8a9d94bd8d 100644
--- a/chrome/browser/ui/ash/web_view/ash_web_view_impl.cc
+++ b/chrome/browser/ui/ash/web_view/ash_web_view_impl.cc
@@ -121,10 +121,9 @@ bool AshWebViewImpl::IsWebContentsCreationOverridden(
content::SiteInstance* source_site_instance,
content::mojom::WindowContainerType window_container_type,
const GURL& opener_url,
- const std::string& frame_name,
- const GURL& target_url) {
+ const content::mojom::CreateNewWindowParams& params) {
if (params_.suppress_navigation) {
- NotifyDidSuppressNavigation(target_url,
+ NotifyDidSuppressNavigation(params.target_url,
WindowOpenDisposition::NEW_FOREGROUND_TAB,
/*from_user_gesture=*/true);
return true;
diff --git a/chrome/browser/ui/ash/web_view/ash_web_view_impl.h b/chrome/browser/ui/ash/web_view/ash_web_view_impl.h
index 39fa45f0a0f9076bd7ac0be6f455dd540a276512..3d0381d463eed73470b28085830f2a23751659a7 100644
--- a/chrome/browser/ui/ash/web_view/ash_web_view_impl.h
+++ b/chrome/browser/ui/ash/web_view/ash_web_view_impl.h
@@ -60,8 +60,7 @@ class AshWebViewImpl : public ash::AshWebView,
content::SiteInstance* source_site_instance,
content::mojom::WindowContainerType window_container_type,
const GURL& opener_url,
- const std::string& frame_name,
- const GURL& target_url) override;
+ const content::mojom::CreateNewWindowParams& params) override;
content::WebContents* OpenURLFromTab(
content::WebContents* source,
const content::OpenURLParams& params,
diff --git a/chrome/browser/ui/browser.cc b/chrome/browser/ui/browser.cc
index 8f8852b2af1acfa4ec985fd1c8b50563b991b12a..c2f2903545b191c5ab13462bf330efce37d7d08c 100644
--- a/chrome/browser/ui/browser.cc
@@ -116,112 +43,6 @@ index acdb28d61badaf549c47e107f4795e1e2adc37c9..b6aca0bf802f2146d09d2a872ff9e091
content::WebContents* CreateCustomWebContents(
content::RenderFrameHost* opener,
content::SiteInstance* source_site_instance,
diff --git a/chrome/browser/ui/media_router/presentation_receiver_window_controller.cc b/chrome/browser/ui/media_router/presentation_receiver_window_controller.cc
index a05510eadf5c9ff24bb7999aa76229946319280f..a80ecc46f8a6b84de83d608257d45ae61ccc2170 100644
--- a/chrome/browser/ui/media_router/presentation_receiver_window_controller.cc
+++ b/chrome/browser/ui/media_router/presentation_receiver_window_controller.cc
@@ -206,8 +206,7 @@ bool PresentationReceiverWindowController::IsWebContentsCreationOverridden(
content::SiteInstance* source_site_instance,
content::mojom::WindowContainerType window_container_type,
const GURL& opener_url,
- const std::string& frame_name,
- const GURL& target_url) {
+ const content::mojom::CreateNewWindowParams& params) {
// Disallow creating separate WebContentses. The WebContents implementation
// uses this to spawn new windows/tabs, which is also not allowed for
// local presentations.
diff --git a/chrome/browser/ui/media_router/presentation_receiver_window_controller.h b/chrome/browser/ui/media_router/presentation_receiver_window_controller.h
index 3fc06be01f20e8cd314d95d73a3f58c2f0742fe9..c07910ae59a185442f37ea6e7b96fdf3a33aba82 100644
--- a/chrome/browser/ui/media_router/presentation_receiver_window_controller.h
+++ b/chrome/browser/ui/media_router/presentation_receiver_window_controller.h
@@ -106,8 +106,7 @@ class PresentationReceiverWindowController final
content::SiteInstance* source_site_instance,
content::mojom::WindowContainerType window_container_type,
const GURL& opener_url,
- const std::string& frame_name,
- const GURL& target_url) override;
+ const content::mojom::CreateNewWindowParams& params) override;
// The profile used for the presentation.
raw_ptr<Profile, DanglingUntriaged> otr_profile_;
diff --git a/chrome/browser/ui/views/hats/hats_next_web_dialog.cc b/chrome/browser/ui/views/hats/hats_next_web_dialog.cc
index 783d05c39ecbe5e556af2e5fd8b4f30fb5aa1196..e1895bcf9d3c6818aa64b1479774a143dae74d53 100644
--- a/chrome/browser/ui/views/hats/hats_next_web_dialog.cc
+++ b/chrome/browser/ui/views/hats/hats_next_web_dialog.cc
@@ -104,8 +104,7 @@ class HatsNextWebDialog::HatsWebView : public views::WebView {
content::SiteInstance* source_site_instance,
content::mojom::WindowContainerType window_container_type,
const GURL& opener_url,
- const std::string& frame_name,
- const GURL& target_url) override {
+ const content::mojom::CreateNewWindowParams& params) override {
return true;
}
content::WebContents* CreateCustomWebContents(
diff --git a/components/embedder_support/android/delegate/web_contents_delegate_android.cc b/components/embedder_support/android/delegate/web_contents_delegate_android.cc
index a82c39208a2709d9e292dac5c89bd2c9bf529a98..d578299501e15815ac615528610889d270aaf6ad 100644
--- a/components/embedder_support/android/delegate/web_contents_delegate_android.cc
+++ b/components/embedder_support/android/delegate/web_contents_delegate_android.cc
@@ -214,15 +214,14 @@ bool WebContentsDelegateAndroid::IsWebContentsCreationOverridden(
content::SiteInstance* source_site_instance,
content::mojom::WindowContainerType window_container_type,
const GURL& opener_url,
- const std::string& frame_name,
- const GURL& target_url) {
+ const content::mojom::CreateNewWindowParams& params) {
JNIEnv* env = AttachCurrentThread();
ScopedJavaLocalRef<jobject> obj = GetJavaDelegate(env);
if (obj.is_null()) {
return false;
}
ScopedJavaLocalRef<jobject> java_gurl =
- url::GURLAndroid::FromNativeGURL(env, target_url);
+ url::GURLAndroid::FromNativeGURL(env, params.target_url.spec());
return !Java_WebContentsDelegateAndroid_shouldCreateWebContents(env, obj,
java_gurl);
}
diff --git a/components/embedder_support/android/delegate/web_contents_delegate_android.h b/components/embedder_support/android/delegate/web_contents_delegate_android.h
index 5754a774852d53a99d34568d0b98aa19171add2a..a75d85c97a75fffa5dba6ac427d7608e345c02ef 100644
--- a/components/embedder_support/android/delegate/web_contents_delegate_android.h
+++ b/components/embedder_support/android/delegate/web_contents_delegate_android.h
@@ -82,8 +82,7 @@ class WebContentsDelegateAndroid : public content::WebContentsDelegate {
content::SiteInstance* source_site_instance,
content::mojom::WindowContainerType window_container_type,
const GURL& opener_url,
- const std::string& frame_name,
- const GURL& target_url) override;
+ const content::mojom::CreateNewWindowParams& params) override;
void CloseContents(content::WebContents* source) override;
bool DidAddMessageToConsole(content::WebContents* source,
blink::mojom::ConsoleMessageLevel log_level,
diff --git a/components/offline_pages/content/background_loader/background_loader_contents.cc b/components/offline_pages/content/background_loader/background_loader_contents.cc
index 12b38ddee62e3af915083830703a4c2e8e249f00..bf4e8dcbdecd46712c48107cfee554b7bb1e0277 100644
--- a/components/offline_pages/content/background_loader/background_loader_contents.cc
+++ b/components/offline_pages/content/background_loader/background_loader_contents.cc
@@ -85,8 +85,7 @@ bool BackgroundLoaderContents::IsWebContentsCreationOverridden(
content::SiteInstance* source_site_instance,
content::mojom::WindowContainerType window_container_type,
const GURL& opener_url,
- const std::string& frame_name,
- const GURL& target_url) {
+ const content::mojom::CreateNewWindowParams& params) {
// Background pages should not create other webcontents/tabs.
return true;
}
diff --git a/components/offline_pages/content/background_loader/background_loader_contents.h b/components/offline_pages/content/background_loader/background_loader_contents.h
index b969f1d97b7e3396119b579cfbe61e19ff7d2dd4..b8d6169652da28266a514938b45b39c58df53573 100644
--- a/components/offline_pages/content/background_loader/background_loader_contents.h
+++ b/components/offline_pages/content/background_loader/background_loader_contents.h
@@ -66,8 +66,7 @@ class BackgroundLoaderContents : public content::WebContentsDelegate {
content::SiteInstance* source_site_instance,
content::mojom::WindowContainerType window_container_type,
const GURL& opener_url,
- const std::string& frame_name,
- const GURL& target_url) override;
+ const content::mojom::CreateNewWindowParams& params) override;
content::WebContents* AddNewContents(
content::WebContents* source,
diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc
index d43e75c20aca09080f4223d339c88381f030c504..8cd59445bae73ff0193e4512d7c36740cbad847f 100644
--- a/content/browser/web_contents/web_contents_impl.cc
@@ -356,48 +177,6 @@ index f459dddeb3f8f3a33ffead0e96fba791d18a0108..f7a229b186774ca3a01f2d747eab139a
content::WebContents* CreateCustomWebContents(
content::RenderFrameHost* opener,
content::SiteInstance* source_site_instance,
diff --git a/fuchsia_web/webengine/browser/frame_impl.cc b/fuchsia_web/webengine/browser/frame_impl.cc
index 9c1fb0b2ed4f013ef6108a9844b22f6bfe697621..ef4991adc766d53b03d280395630b83ced38c2e8 100644
--- a/fuchsia_web/webengine/browser/frame_impl.cc
+++ b/fuchsia_web/webengine/browser/frame_impl.cc
@@ -585,8 +585,7 @@ bool FrameImpl::IsWebContentsCreationOverridden(
content::SiteInstance* source_site_instance,
content::mojom::WindowContainerType window_container_type,
const GURL& opener_url,
- const std::string& frame_name,
- const GURL& target_url) {
+ const content::mojom::CreateNewWindowParams& params) {
// Specify a generous upper bound for unacknowledged popup windows, so that we
// can catch bad client behavior while not interfering with normal operation.
constexpr size_t kMaxPendingWebContentsCount = 10;
diff --git a/fuchsia_web/webengine/browser/frame_impl.h b/fuchsia_web/webengine/browser/frame_impl.h
index 756d4192271d6a65cfe8e1511737c565b543cb1f..5688f6f745056565c3c01947f741c4d13e27b6ae 100644
--- a/fuchsia_web/webengine/browser/frame_impl.h
+++ b/fuchsia_web/webengine/browser/frame_impl.h
@@ -308,8 +308,7 @@ class WEB_ENGINE_EXPORT FrameImpl : public fuchsia::web::Frame,
content::SiteInstance* source_site_instance,
content::mojom::WindowContainerType window_container_type,
const GURL& opener_url,
- const std::string& frame_name,
- const GURL& target_url) override;
+ const content::mojom::CreateNewWindowParams& params) override;
void WebContentsCreated(content::WebContents* source_contents,
int opener_render_process_id,
int opener_render_frame_id,
diff --git a/headless/lib/browser/headless_web_contents_impl.cc b/headless/lib/browser/headless_web_contents_impl.cc
index ae616fa9c352413e23fb509b3e12e0e4fab5a094..0efa65f7d4346cfe78d2f27ba55a0526202315ff 100644
--- a/headless/lib/browser/headless_web_contents_impl.cc
+++ b/headless/lib/browser/headless_web_contents_impl.cc
@@ -232,8 +232,7 @@ class HeadlessWebContentsImpl::Delegate : public content::WebContentsDelegate {
content::SiteInstance* source_site_instance,
content::mojom::WindowContainerType window_container_type,
const GURL& opener_url,
- const std::string& frame_name,
- const GURL& target_url) override {
+ const content::mojom::CreateNewWindowParams& params) override {
return headless_web_contents_->browser_context()
->options()
->block_new_web_contents();
diff --git a/ui/views/controls/webview/web_dialog_view.cc b/ui/views/controls/webview/web_dialog_view.cc
index a7d370220136f2c31afd70644ada26f1768b2e0d..e08dd61b20c68398b0532f5ae74e0ffd5968c19b 100644
--- a/ui/views/controls/webview/web_dialog_view.cc

View File

@@ -1,2 +1,3 @@
chore_expose_ui_to_allow_electron_to_set_dock_side.patch
feat_allow_enabling_extension_panels_on_custom_protocols.patch
fix_context_selector_not_showing_execution_contexts.patch

View File

@@ -0,0 +1,25 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Fedor Indutny <indutny@signal.org>
Date: Tue, 14 Apr 2026 19:33:23 -0700
Subject: Fix context selector not showing execution contexts
When execution context's origin is `file://` - the `url.domain()` is an
empty string and the item's subtitle becomes falsy. Since we predicated
rendering of an item on presence of both title and subtitle - the items
were not rendered while still taking space in the dropdown.
Pending CL: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/7761316
diff --git a/front_end/panels/console/ConsoleContextSelector.ts b/front_end/panels/console/ConsoleContextSelector.ts
index f933dbd6dbdfadd2ea8b78e473d9506350825037..d8aa3f9811f1857a9b30231ed8ff59e709e3383c 100644
--- a/front_end/panels/console/ConsoleContextSelector.ts
+++ b/front_end/panels/console/ConsoleContextSelector.ts
@@ -303,7 +303,7 @@ interface ViewInput {
type View = (input: ViewInput, output: undefined, target: HTMLElement) => void;
const DEFAULT_VIEW: View = (input, _output, target): void => {
- if (!input.title || !input.subtitle) {
+ if (!input.title) {
render(nothing, target);
return;
}

View File

@@ -12,3 +12,4 @@ use_uttype_class_instead_of_deprecated_uttypeconformsto.patch
fix_clean_up_orphaned_staged_updates_before_downloading_new_update.patch
fix_add_explicit_json_property_mappings_for_shipit_request_model.patch
fix_resolve_target_bundle_path_once_at_start_of_install.patch
fix_remove_vestigial_machservices_from_shipit_launchd_job.patch

View File

@@ -0,0 +1,49 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Keeley Hammond <vertedinde@electronjs.org>
Date: Tue, 14 Apr 2026 10:00:00 -0700
Subject: fix: remove vestigial MachServices from ShipIt launchd job
The MachServices key in the ShipIt launchd job dictionary is a leftover
from when ShipIt was an XPC service (removed in Squirrel/Squirrel.Mac@d6ca1c2
":fire: XPC :fire:" in October 2013). Since then, nothing connects to
the registered Mach port.
When a macOS system update is pending, launchd puts the user domain into
"on-demand-only mode", where only jobs with an on-demand trigger (like a
MachServices connection) are started. Since nothing connects to ShipIt's
Mach port, launchd pends the spawn indefinitely with:
"pending spawn, domain in on-demand-only mode"
This prevents ShipIt from running, causing Electron auto-updates to fail
whenever a macOS update is staged. By the time the domain exits
on-demand-only mode (after reboot/update completion), the staged update
bundle is often stale, leading to "Too many attempts to install" errors.
Removing the MachServices key eliminates the on-demand trigger, so
launchd falls back to evaluating KeepAlive.SuccessfulExit which
correctly starts ShipIt immediately on submission.
Also removes the stale "service name" comment on the jobLabel argument,
since ShipIt is no longer a Mach service.
diff --git a/Squirrel/SQRLShipItLauncher.m b/Squirrel/SQRLShipItLauncher.m
index 6a9151d92f399184fff9854eb00ea506165bbbe2..0ebd2a23d62424c41e15413edeab360bef87ecc4 100644
--- a/Squirrel/SQRLShipItLauncher.m
+++ b/Squirrel/SQRLShipItLauncher.m
@@ -50,14 +50,10 @@ + (RACSignal *)shipItJobDictionary {
@(LAUNCH_JOBKEY_KEEPALIVE_SUCCESSFULEXIT): @NO
};
- jobDict[@(LAUNCH_JOBKEY_MACHSERVICES)] = @{
- jobLabel: @YES
- };
-
NSMutableArray *arguments = [[NSMutableArray alloc] init];
[arguments addObject:[squirrelBundle URLForResource:@"ShipIt" withExtension:nil].path];
- // Pass in the service name so ShipIt knows how to broadcast itself.
+ // Pass in the job label so ShipIt can identify itself.
[arguments addObject:jobLabel];
// We need to pass the path to ShipIt rather than having ShipIt

View File

@@ -5,18 +5,36 @@ import { ElectronReleaseRepo } from './types';
const cachedTokens = Object.create(null);
const SUDOWOODO_OIDC_AUDIENCE = 'sudowoodo-broker';
async function getActionsIdToken (): Promise<string> {
const { ACTIONS_ID_TOKEN_REQUEST_URL, ACTIONS_ID_TOKEN_REQUEST_TOKEN } = process.env;
if (!ACTIONS_ID_TOKEN_REQUEST_URL || !ACTIONS_ID_TOKEN_REQUEST_TOKEN) {
throw new Error(
'ACTIONS_ID_TOKEN_REQUEST_URL/_TOKEN not set — the job needs `permissions: id-token: write` to mint an OIDC token for the sudowoodo exchange'
);
}
const { value } = await got(ACTIONS_ID_TOKEN_REQUEST_URL + '&audience=' + SUDOWOODO_OIDC_AUDIENCE, {
headers: {
authorization: 'Bearer ' + ACTIONS_ID_TOKEN_REQUEST_TOKEN
}
}).json<{ value: string }>();
return value;
}
async function ensureToken (repo: ElectronReleaseRepo) {
if (!cachedTokens[repo]) {
cachedTokens[repo] = await (async () => {
const { ELECTRON_GITHUB_TOKEN, SUDOWOODO_EXCHANGE_URL, SUDOWOODO_EXCHANGE_TOKEN } = process.env;
const { ELECTRON_GITHUB_TOKEN, SUDOWOODO_EXCHANGE_URL } = process.env;
if (ELECTRON_GITHUB_TOKEN) {
return ELECTRON_GITHUB_TOKEN;
}
if (SUDOWOODO_EXCHANGE_URL && SUDOWOODO_EXCHANGE_TOKEN) {
if (SUDOWOODO_EXCHANGE_URL) {
const idToken = await getActionsIdToken();
const resp = await got.post(SUDOWOODO_EXCHANGE_URL + '?repo=' + repo, {
headers: {
Authorization: SUDOWOODO_EXCHANGE_TOKEN
Authorization: 'Bearer ' + idToken
},
throwHttpErrors: false
});

View File

@@ -32,6 +32,7 @@
#if BUILDFLAG(ENABLE_PDF_VIEWER)
#include "components/pdf/common/constants.h" // nogncheck
#include "components/pdf/common/pdf_util.h" // nogncheck
#include "shell/common/electron_constants.h"
#endif // BUILDFLAG(ENABLE_PDF_VIEWER)
@@ -217,4 +218,13 @@ void ElectronContentClient::AddContentDecryptionModules(
}
}
bool ElectronContentClient::IsFilePickerAllowedForCrossOriginSubframe(
const url::Origin& origin) {
#if BUILDFLAG(ENABLE_PDF_VIEWER)
return IsPdfExtensionOrigin(origin);
#else
return false;
#endif
}
} // namespace electron

View File

@@ -9,6 +9,7 @@
#include <vector>
#include "content/public/common/content_client.h"
#include "url/origin.h"
namespace electron {
@@ -33,6 +34,8 @@ class ElectronContentClient : public content::ContentClient {
void AddContentDecryptionModules(
std::vector<content::CdmInfo>* cdms,
std::vector<media::CdmHostFilePath>* cdm_host_file_paths) override;
bool IsFilePickerAllowedForCrossOriginSubframe(
const url::Origin& origin) override;
};
} // namespace electron

View File

@@ -8,7 +8,6 @@
#include "extensions/browser/extension_registry.h"
#include "gin/data_object_builder.h"
#include "gin/object_template_builder.h"
#include "shell/browser/api/electron_api_extensions.h"
#include "shell/browser/electron_browser_context.h"
#include "shell/browser/extensions/electron_extension_system.h"
#include "shell/browser/javascript_environment.h"
@@ -17,17 +16,17 @@
#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/handle.h"
#include "shell/common/gin_helper/promise.h"
#include "shell/common/node_util.h"
#include "v8/include/cppgc/allocation.h"
namespace electron::api {
gin::DeprecatedWrapperInfo Extensions::kWrapperInfo = {gin::kEmbedderNativeGin};
const gin::WrapperInfo Extensions::kWrapperInfo = {{gin::kEmbedderNativeGin},
gin::kElectronExtensions};
Extensions::Extensions(v8::Isolate* isolate,
ElectronBrowserContext* browser_context)
: browser_context_(browser_context) {
Extensions::Extensions(ElectronBrowserContext* browser_context)
: browser_context_{browser_context} {
extensions::ExtensionRegistry::Get(browser_context)->AddObserver(this);
}
@@ -36,11 +35,10 @@ Extensions::~Extensions() {
}
// static
gin_helper::Handle<Extensions> Extensions::Create(
v8::Isolate* isolate,
ElectronBrowserContext* browser_context) {
return gin_helper::CreateHandle(isolate,
new Extensions(isolate, browser_context));
Extensions* Extensions::Create(v8::Isolate* isolate,
ElectronBrowserContext* browser_context) {
return cppgc::MakeGarbageCollected<Extensions>(
isolate->GetCppHeap()->GetAllocationHandle(), browser_context);
}
v8::Local<v8::Promise> Extensions::LoadExtension(
@@ -152,8 +150,12 @@ gin::ObjectTemplateBuilder Extensions::GetObjectTemplateBuilder(
.SetMethod("getAllExtensions", &Extensions::GetAllExtensions);
}
const char* Extensions::GetTypeName() {
return "Extensions";
const gin::WrapperInfo* Extensions::wrapper_info() const {
return &kWrapperInfo;
}
const char* Extensions::GetHumanReadableName() const {
return "Electron / Extensions";
}
} // namespace electron::api

View File

@@ -6,15 +6,9 @@
#define ELECTRON_SHELL_BROWSER_API_ELECTRON_API_EXTENSIONS_H_
#include "base/memory/raw_ptr.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_registry_observer.h"
#include "gin/wrappable.h"
#include "shell/browser/event_emitter_mixin.h"
#include "shell/common/gin_helper/wrappable.h"
namespace gin_helper {
template <typename T>
class Handle;
} // namespace gin_helper
namespace electron {
@@ -22,19 +16,24 @@ class ElectronBrowserContext;
namespace api {
class Extensions final : public gin_helper::DeprecatedWrappable<Extensions>,
class Extensions final : public gin::Wrappable<Extensions>,
public gin_helper::EventEmitterMixin<Extensions>,
private extensions::ExtensionRegistryObserver {
public:
static gin_helper::Handle<Extensions> Create(
v8::Isolate* isolate,
ElectronBrowserContext* browser_context);
static Extensions* Create(v8::Isolate* isolate,
ElectronBrowserContext* browser_context);
// gin_helper::Wrappable
static gin::DeprecatedWrapperInfo kWrapperInfo;
// gin::Wrappable
static const gin::WrapperInfo kWrapperInfo;
const gin::WrapperInfo* wrapper_info() const override;
const char* GetHumanReadableName() const override;
gin::ObjectTemplateBuilder GetObjectTemplateBuilder(
v8::Isolate* isolate) override;
const char* GetTypeName() override;
const char* GetClassName() const { return "Extensions"; }
// Make public for cppgc::MakeGarbageCollected.
explicit Extensions(ElectronBrowserContext* browser_context);
~Extensions() override;
v8::Local<v8::Promise> LoadExtension(v8::Isolate* isolate,
const base::FilePath& extension_path,
@@ -57,17 +56,12 @@ class Extensions final : public gin_helper::DeprecatedWrappable<Extensions>,
Extensions(const Extensions&) = delete;
Extensions& operator=(const Extensions&) = delete;
protected:
explicit Extensions(v8::Isolate* isolate,
ElectronBrowserContext* browser_context);
~Extensions() override;
private:
content::BrowserContext* browser_context() const {
return browser_context_.get();
}
raw_ptr<content::BrowserContext> browser_context_;
const raw_ptr<content::BrowserContext> browser_context_;
};
} // namespace api

View File

@@ -24,6 +24,7 @@
#include <windows.h>
#include "base/no_destructor.h"
#include "base/strings/utf_string_conversions.h"
#include "shell/browser/javascript_environment.h"
#include "shell/browser/notifications/win/windows_toast_activator.h"
#endif
@@ -76,6 +77,7 @@ Notification::Notification(gin::Arguments* args) {
if (args->GetNext(&opts)) {
opts.Get("id", &id_);
opts.Get("groupId", &group_id_);
opts.Get("groupTitle", &group_title_);
opts.Get("title", &title_);
opts.Get("subtitle", &subtitle_);
opts.Get("body", &body_);
@@ -108,7 +110,32 @@ gin_helper::Handle<Notification> Notification::New(
thrower.ThrowError("Cannot create Notification before app is ready");
return {};
}
return gin_helper::CreateHandle(thrower.isolate(), new Notification(args));
auto handle =
gin_helper::CreateHandle(thrower.isolate(), new Notification(args));
#if BUILDFLAG(IS_WIN)
constexpr size_t kMaxTagLength = 64;
auto* notif = handle.get();
if (!notif->id_.empty() &&
base::UTF8ToWide(notif->id_).length() > kMaxTagLength) {
thrower.ThrowError(
"Notification id exceeds Windows limit of 64 UTF-16 characters");
return {};
}
if (!notif->group_id_.empty() &&
base::UTF8ToWide(notif->group_id_).length() > kMaxTagLength) {
thrower.ThrowError(
"Notification groupId exceeds Windows limit of 64 UTF-16 characters");
return {};
}
if (!notif->group_title_.empty() && notif->group_id_.empty()) {
thrower.ThrowError("Notification groupTitle requires groupId to be set");
return {};
}
#endif
return handle;
}
// Setters
@@ -259,6 +286,7 @@ void Notification::Show() {
options.urgency = urgency_;
options.toast_xml = toast_xml_;
options.group_id = group_id_;
options.group_title = group_title_;
notification_->Show(options);
}
}
@@ -349,6 +377,7 @@ void Notification::FillObjectTemplate(v8::Isolate* isolate,
.SetMethod("close", &Notification::Close)
.SetProperty("id", &Notification::id)
.SetProperty("groupId", &Notification::group_id)
.SetProperty("groupTitle", &Notification::group_title)
.SetProperty("title", &Notification::title, &Notification::SetTitle)
.SetProperty("subtitle", &Notification::subtitle,
&Notification::SetSubtitle)

View File

@@ -85,6 +85,7 @@ class Notification final : public gin_helper::DeprecatedWrappable<Notification>,
// Prop Getters
const std::string& id() const { return id_; }
const std::string& group_id() const { return group_id_; }
const std::u16string& group_title() const { return group_title_; }
const std::u16string& title() const { return title_; }
const std::u16string& subtitle() const { return subtitle_; }
const std::u16string& body() const { return body_; }
@@ -117,6 +118,7 @@ class Notification final : public gin_helper::DeprecatedWrappable<Notification>,
private:
std::string id_;
std::string group_id_;
std::u16string group_title_;
std::u16string title_;
std::u16string subtitle_;
std::u16string body_;

View File

@@ -1343,15 +1343,12 @@ v8::Local<v8::Value> Session::Cookies(v8::Isolate* isolate) {
return cookies_.Get(isolate);
}
v8::Local<v8::Value> Session::Extensions(v8::Isolate* isolate) {
api::Extensions* Session::Extensions(v8::Isolate* isolate) {
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
if (extensions_.IsEmptyThreadSafe()) {
v8::Local<v8::Value> handle;
handle = Extensions::Create(isolate, browser_context()).ToV8();
extensions_.Reset(isolate, handle);
}
if (!extensions_)
extensions_ = Extensions::Create(isolate, browser_context());
#endif
return extensions_.Get(isolate);
return extensions_.Get();
}
api::Protocol* Session::Protocol() {

View File

@@ -57,6 +57,7 @@ struct PreloadScript;
namespace api {
class Extensions;
class NetLog;
class Protocol;
class ServiceWorkerContext;
@@ -166,7 +167,7 @@ class Session final : public gin::Wrappable<Session>,
v8::Local<v8::Promise> ClearSharedDictionaryCacheForIsolationKey(
const gin_helper::Dictionary& options);
v8::Local<v8::Value> Cookies(v8::Isolate* isolate);
v8::Local<v8::Value> Extensions(v8::Isolate* isolate);
api::Extensions* Extensions(v8::Isolate* isolate);
api::Protocol* Protocol();
api::ServiceWorkerContext* ServiceWorkerContext();
WebRequest* WebRequest(v8::Isolate* isolate);
@@ -213,7 +214,7 @@ class Session final : public gin::Wrappable<Session>,
// Cached gin_helper::Wrappable objects.
v8::TracedReference<v8::Value> cookies_;
v8::TracedReference<v8::Value> extensions_;
cppgc::Member<api::Extensions> extensions_;
cppgc::Member<api::Protocol> protocol_;
cppgc::Member<api::NetLog> net_log_;
cppgc::Member<api::ServiceWorkerContext> service_worker_context_;

View File

@@ -1176,7 +1176,9 @@ void WebContents::DeleteThisIfAlive() {
void WebContents::Destroy() {
// The content::WebContents should be destroyed asynchronously when possible
// as user may choose to destroy WebContents during an event of it.
if (Browser::Get()->is_shutting_down() || is_guest()) {
if (Browser::Get()->is_shutting_down()) {
DeleteThisIfAlive();
} else if (is_guest() && is_safe_to_delete_ && !is_emitting_event_) {
DeleteThisIfAlive();
} else {
content::GetUIThreadTaskRunner({})->PostTask(
@@ -1436,17 +1438,21 @@ void WebContents::BeforeUnloadFired(content::WebContents* tab,
void WebContents::SetContentsBounds(content::WebContents* source,
const gfx::Rect& rect) {
base::AutoReset<bool> resetter(&is_emitting_event_, true);
if (!Emit("content-bounds-updated", rect))
observers_.Notify(&ExtendedWebContentsObserver::OnSetContentBounds, rect);
}
void WebContents::CloseContents(content::WebContents* source) {
Emit("close");
{
base::AutoReset<bool> resetter(&is_emitting_event_, true);
Emit("close");
auto* autofill_driver_factory =
AutofillDriverFactory::FromWebContents(web_contents());
if (autofill_driver_factory) {
autofill_driver_factory->CloseAllPopups();
auto* autofill_driver_factory =
AutofillDriverFactory::FromWebContents(web_contents());
if (autofill_driver_factory) {
autofill_driver_factory->CloseAllPopups();
}
}
Destroy();
@@ -1959,6 +1965,7 @@ void WebContents::FrameDeleted(content::FrameTreeNodeId frame_tree_node_id) {
}
void WebContents::RenderViewDeleted(content::RenderViewHost* render_view_host) {
base::AutoReset<bool> resetter(&is_emitting_event_, true);
const auto id = render_view_host->GetProcess()->GetID().GetUnsafeValue();
// This event is necessary for tracking any states with respect to
// intermediate render view hosts aka speculative render view hosts. Currently
@@ -2198,6 +2205,9 @@ void WebContents::DidFinishNavigation(
if (!navigation_handle->HasCommitted())
return;
base::AutoReset<bool> resetter(&is_emitting_event_, true);
bool is_main_frame = navigation_handle->IsInMainFrame();
content::RenderFrameHost* frame_host =
navigation_handle->GetRenderFrameHost();
@@ -2207,9 +2217,6 @@ void WebContents::DidFinishNavigation(
frame_routing_id = frame_host->GetRoutingID();
}
if (!navigation_handle->IsErrorPage()) {
// FIXME: All the Emit() calls below could potentially result in |this|
// being destroyed (by JS listening for the event and calling
// webContents.destroy()).
auto url = navigation_handle->GetURL();
bool is_same_document = navigation_handle->IsSameDocument();
if (is_same_document) {
@@ -2312,9 +2319,16 @@ void WebContents::DevToolsOpened() {
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
v8::HandleScope handle_scope(isolate);
DCHECK(inspectable_web_contents_);
DCHECK(inspectable_web_contents_->GetDevToolsWebContents());
auto handle = FromOrCreate(
isolate, inspectable_web_contents_->GetDevToolsWebContents());
// GetDevToolsWebContents() can be null here when DevTools were closed
// re-entrantly during InspectableWebContents::LoadCompleted() — e.g. when
// a JS handler for `devtools-focused` (fired from the activate path inside
// SetIsDocked) calls closeDevTools() before LoadCompleted finishes.
content::WebContents* const dtwc = GetDevToolsWebContents();
if (!dtwc)
return;
auto handle = FromOrCreate(isolate, dtwc);
devtools_web_contents_.Reset(isolate, handle.ToV8());
// Set inspected tabID.
@@ -2322,12 +2336,11 @@ void WebContents::DevToolsOpened() {
"DevToolsAPI", "setInspectedTabId", base::Value(ID()));
// Inherit owner window in devtools when it doesn't have one.
auto* devtools = inspectable_web_contents_->GetDevToolsWebContents();
bool has_window = devtools->GetUserData(NativeWindowRelay::UserDataKey());
if (owner_window() && !has_window) {
CHECK(!owner_window_.WasInvalidated());
bool has_window = dtwc->GetUserData(NativeWindowRelay::UserDataKey());
if (owner_window_ && !has_window) {
DCHECK(!owner_window_.WasInvalidated());
DCHECK_EQ(handle->owner_window(), nullptr);
handle->SetOwnerWindow(devtools, owner_window());
handle->SetOwnerWindow(dtwc, owner_window());
}
Emit("devtools-opened");
@@ -3013,7 +3026,7 @@ void WebContents::InspectElement(int x, int y) {
if (!enable_devtools_ || !inspectable_web_contents_)
return;
if (!inspectable_web_contents_->GetDevToolsWebContents())
if (!GetDevToolsWebContents())
OpenDevTools(nullptr);
inspectable_web_contents_->InspectElement(x, y);
}

View File

@@ -876,9 +876,15 @@ class WebContents final : public ExclusiveAccessContext,
const scoped_refptr<base::TaskRunner> print_task_runner_;
#endif
// Track navigation state in order to avoid potential re-entrancy crashes.
// Track navigation state in order to avoid potential re-entrancy crashes
// in LoadURL. Checked by LoadURL to reject re-entrant navigation attempts.
bool is_safe_to_delete_ = true;
// Set to true while dispatching JS events via Emit(). When true, Destroy()
// defers guest WebContents deletion to prevent use-after-free when a JS
// handler calls webContents.destroy() mid-emission.
bool is_emitting_event_ = false;
// Stores the frame that's currently in fullscreen, nullptr if there is none.
raw_ptr<content::RenderFrameHost> fullscreen_frame_ = nullptr;

View File

@@ -618,6 +618,8 @@ void ElectronBrowserMainParts::PostMainMessageLoopRun() {
node_bindings_->set_uv_env(nullptr);
node_env_.reset();
browser_.reset();
js_env_.reset();
ElectronBrowserContext::DestroyAllContexts();
fake_browser_process_->PostMainMessageLoopRun();
@@ -626,9 +628,6 @@ void ElectronBrowserMainParts::PostMainMessageLoopRun() {
#if BUILDFLAG(IS_LINUX)
ui::OzonePlatform::GetInstance()->PostMainMessageLoopRun();
#endif
browser_.reset();
js_env_.reset();
}
#if !BUILDFLAG(IS_MAC)

View File

@@ -50,6 +50,7 @@ struct NotificationOptions {
std::u16string close_button_text;
std::u16string toast_xml;
std::string group_id;
std::u16string group_title;
NotificationOptions();
NotificationOptions(const NotificationOptions&);

View File

@@ -539,9 +539,8 @@ void HandleToastActivation(const std::wstring& invoked_args,
Notification* target = nullptr;
for (auto* n : presenter->notifications()) {
std::wstring tag_hash =
base::NumberToWString(base::FastHash(n->notification_id()));
if (tag_hash == tag_str) {
std::wstring tag = base::UTF8ToWide(n->notification_id());
if (tag == tag_str) {
target = n;
break;
}

View File

@@ -71,7 +71,7 @@ void DebugLog(std::string_view log_msg) {
}
std::wstring GetTag(const std::string_view notification_id) {
return base::NumberToWString(base::FastHash(notification_id));
return base::UTF8ToWide(notification_id);
}
// See https://www.hresult.info for HRESULT error codes.
@@ -292,7 +292,7 @@ void WindowsToastNotification::CreateToastNotificationOnBackgroundThread(
ui_runner->PostTask(
FROM_HERE,
base::BindOnce(&WindowsToastNotification::SetupAndShowOnUIThread,
weak_notification, toast_notification));
weak_notification, toast_notification, options.group_id));
}
// Creates the toast XML document on the background thread. Returns true on
@@ -327,7 +327,8 @@ bool WindowsToastNotification::CreateToastXmlDocument(
std::u16string toast_xml_str =
GetToastXml(notification_id, options.title, options.msg, icon_path,
options.timeout_type, options.silent, options.actions,
options.has_reply, options.reply_placeholder);
options.has_reply, options.reply_placeholder,
options.group_id, options.group_title);
DebugLog("Toast XML (generated) id=" + notification_id + ": " +
base::UTF16ToUTF8(toast_xml_str));
@@ -398,7 +399,13 @@ bool WindowsToastNotification::CreateToastNotification(
return false;
}
ScopedHString group(kGroup);
// Use provided group_id if non-empty, otherwise fall back to default
std::wstring group_str =
options.group_id.empty() ? kGroup : base::UTF8ToWide(options.group_id);
ScopedHString group(group_str);
DebugLog(
base::StrCat({"Setting group: ", base::WideToUTF8(group_str),
options.group_id.empty() ? " (default)" : " (custom)"}));
hr = toast2->put_Group(group);
if (FAILED(hr)) {
std::string err = base::StrCat(
@@ -408,7 +415,10 @@ bool WindowsToastNotification::CreateToastNotification(
return false;
}
ScopedHString tag(GetTag(notification_id));
std::wstring tag_str = GetTag(notification_id);
ScopedHString tag(tag_str);
DebugLog(base::StrCat({"Setting tag: ", base::WideToUTF8(tag_str),
" (from id: ", notification_id, ")"}));
hr = toast2->put_Tag(tag);
if (FAILED(hr)) {
std::string err = base::StrCat(
@@ -447,7 +457,8 @@ bool WindowsToastNotification::CreateToastNotification(
// does not allow calls from background threads.
void WindowsToastNotification::SetupAndShowOnUIThread(
base::WeakPtr<Notification> weak_notification,
ComPtr<ABI::Windows::UI::Notifications::IToastNotification> notification) {
ComPtr<ABI::Windows::UI::Notifications::IToastNotification> notification,
const std::string& group_id) {
auto* notif = static_cast<WindowsToastNotification*>(weak_notification.get());
if (!notif)
return;
@@ -463,6 +474,7 @@ void WindowsToastNotification::SetupAndShowOnUIThread(
}
notif->toast_notification_ = notification;
notif->group_id_ = group_id;
// Show notification on UI thread (must be called on UI thread)
hr = (*toast_notifier_)->Show(notification.Get());
@@ -538,8 +550,14 @@ void WindowsToastNotification::Remove() {
if (!GetAppUserModelID(&app_id))
return;
ScopedHString group(kGroup);
ScopedHString tag(GetTag(notification_id()));
// Use stored group_id if set, otherwise fall back to default
std::wstring group_str =
group_id_.empty() ? kGroup : base::UTF8ToWide(group_id_);
ScopedHString group(group_str);
std::wstring tag_str = GetTag(notification_id());
ScopedHString tag(tag_str);
DebugLog(base::StrCat({"Removing with group: ", base::WideToUTF8(group_str),
", tag: ", base::WideToUTF8(tag_str)}));
notification_history->RemoveGroupedTagWithId(tag, group, app_id);
}
@@ -558,7 +576,9 @@ std::u16string WindowsToastNotification::GetToastXml(
bool silent,
const std::vector<NotificationAction>& actions,
bool has_reply,
const std::u16string& reply_placeholder) {
const std::u16string& reply_placeholder,
const std::string& group_id,
const std::u16string& group_title) {
XmlWriter xml_writer;
xml_writer.StartWriting();
@@ -570,6 +590,15 @@ std::u16string WindowsToastNotification::GetToastXml(
xml_writer.AddAttribute(kScenario, kReminder);
}
// Add header element if both groupId and groupTitle are present
if (!group_id.empty() && !group_title.empty()) {
xml_writer.StartElement("header");
xml_writer.AddAttribute("id", group_id);
xml_writer.AddAttribute("title", base::UTF16ToUTF8(group_title));
xml_writer.AddAttribute("arguments", "");
xml_writer.EndElement(); // </header>
}
// <visual>
xml_writer.StartElement(kVisual);
// <binding template="<template>">
@@ -642,9 +671,9 @@ std::u16string WindowsToastNotification::GetToastXml(
// <action>
xml_writer.StartElement(kAction);
xml_writer.AddAttribute(kActivationType, kActivationTypeForeground);
std::string args = base::StrCat(
{"type=action&action=", base::NumberToString(i),
"&tag=", base::NumberToString(base::FastHash(notification_id))});
std::string args =
base::StrCat({"type=action&action=", base::NumberToString(i),
"&tag=", notification_id});
xml_writer.AddAttribute(kArguments, args.c_str());
xml_writer.AddAttribute(kContent, base::UTF16ToUTF8(act.text));
xml_writer.EndElement(); // <action>
@@ -666,9 +695,9 @@ std::u16string WindowsToastNotification::GetToastXml(
// The button that submits the selection.
xml_writer.StartElement(kAction);
xml_writer.AddAttribute(kActivationType, kActivationTypeForeground);
std::string args = base::StrCat(
{"type=action&action=", base::NumberToString(i),
"&tag=", base::NumberToString(base::FastHash(notification_id))});
std::string args =
base::StrCat({"type=action&action=", base::NumberToString(i),
"&tag=", notification_id});
xml_writer.AddAttribute(kArguments, args.c_str());
xml_writer.AddAttribute(
kContent,
@@ -682,9 +711,7 @@ std::u16string WindowsToastNotification::GetToastXml(
// <action>
xml_writer.StartElement(kAction);
xml_writer.AddAttribute(kActivationType, kActivationTypeForeground);
std::string args =
base::StrCat({"type=reply&tag=",
base::NumberToString(base::FastHash(notification_id))});
std::string args = base::StrCat({"type=reply&tag=", notification_id});
xml_writer.AddAttribute(kArguments, args.c_str());
// TODO(codebytere): we should localize this.
xml_writer.AddAttribute(kContent, "Reply");
@@ -800,10 +827,8 @@ IFACEMETHODIMP ToastEventHandler::Invoke(
}
std::string notif_id;
std::string notif_hash;
if (notification_) {
notif_id = notification_->notification_id();
notif_hash = base::NumberToString(base::FastHash(notif_id));
}
bool structured = arguments_w.find(L"&tag=") != std::wstring::npos ||

View File

@@ -69,7 +69,9 @@ class WindowsToastNotification : public Notification {
const bool silent,
const std::vector<NotificationAction>& actions,
bool has_reply,
const std::u16string& reply_placeholder);
const std::u16string& reply_placeholder,
const std::string& group_id,
const std::u16string& group_title);
static HRESULT XmlDocumentFromString(
const wchar_t* xmlString,
ABI::Windows::Data::Xml::Dom::IXmlDocument** doc);
@@ -102,7 +104,8 @@ class WindowsToastNotification : public Notification {
toast_notification);
static void SetupAndShowOnUIThread(
base::WeakPtr<Notification> weak_notification,
ComPtr<ABI::Windows::UI::Notifications::IToastNotification> notification);
ComPtr<ABI::Windows::UI::Notifications::IToastNotification> notification,
const std::string& group_id);
static void PostNotificationFailedToUIThread(
base::WeakPtr<Notification> weak_notification,
const std::string& error,
@@ -124,6 +127,9 @@ class WindowsToastNotification : public Notification {
ComPtr<ToastEventHandler> event_handler_;
ComPtr<ABI::Windows::UI::Notifications::IToastNotification>
toast_notification_;
// Stored for Remove() to use when removing from Action Center
std::string group_id_;
};
class ToastEventHandler : public RuntimeClass<RuntimeClassFlags<ClassicCom>,

View File

@@ -11,6 +11,7 @@
#include <string_view>
#include <utility>
#include "base/auto_reset.h"
#include "base/base64.h"
#include "base/containers/fixed_flat_set.h"
#include "base/containers/span.h"
@@ -448,13 +449,17 @@ void InspectableWebContents::ShowDevTools(bool activate) {
}
void InspectableWebContents::CloseDevTools() {
if (is_showing_devtools_) {
close_devtools_pending_ = true;
return;
}
if (GetDevToolsWebContents()) {
frontend_loaded_ = false;
embedder_message_dispatcher_.reset();
if (managed_devtools_web_contents_) {
view_->CloseDevTools();
managed_devtools_web_contents_.reset();
}
embedder_message_dispatcher_.reset();
if (!is_guest())
web_contents_->Focus();
}
@@ -549,52 +554,74 @@ void InspectableWebContents::CloseWindow() {
void InspectableWebContents::LoadCompleted() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
frontend_loaded_ = true;
if (managed_devtools_web_contents_)
view_->ShowDevTools(activate_);
if (!GetDevToolsWebContents())
return;
// If the devtools can dock, "SetIsDocked" will be called by devtools itself.
if (!can_dock_) {
SetIsDocked(DispatchCallback(), false);
if (!devtools_title_.empty()) {
view_->SetTitle(devtools_title_);
}
} else {
if (dock_state_.empty()) {
const base::DictValue& prefs =
pref_service_->GetDict(kDevToolsPreferences);
const std::string* current_dock_state =
prefs.FindString("currentDockState");
if (current_dock_state) {
std::string sanitized;
base::RemoveChars(*current_dock_state, "\"", &sanitized);
dock_state_ = IsValidDockState(sanitized) ? sanitized : "right";
} else {
dock_state_ = "right";
frontend_loaded_ = true;
// ShowDevTools and SetIsDocked trigger focus on the DevTools WebContents.
// Focus events fire JS handlers via V8 microtask checkpoints, and those
// handlers can call closeDevTools() re-entrantly. Guard the entire show
// phase so that any re-entrant close is deferred until the stack unwinds.
{
base::AutoReset<bool> guard(&is_showing_devtools_, true);
if (managed_devtools_web_contents_)
view_->ShowDevTools(activate_);
// If the devtools can dock, "SetIsDocked" will be called by devtools
// itself.
if (!can_dock_) {
SetIsDocked(DispatchCallback(), false);
if (!devtools_title_.empty()) {
view_->SetTitle(devtools_title_);
}
}
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX)
auto* api_web_contents = api::WebContents::From(GetWebContents());
if (api_web_contents) {
auto* win =
static_cast<NativeWindowViews*>(api_web_contents->owner_window());
// When WCO is enabled, undock the devtools if the current dock
// position overlaps with the position of window controls to avoid
// broken layout.
if (win && win->IsWindowControlsOverlayEnabled()) {
if (IsAppRTL() && dock_state_ == "left") {
dock_state_ = "undocked";
} else if (dock_state_ == "right") {
dock_state_ = "undocked";
} else {
if (dock_state_.empty()) {
const base::DictValue& prefs =
pref_service_->GetDict(kDevToolsPreferences);
const std::string* current_dock_state =
prefs.FindString("currentDockState");
if (current_dock_state) {
std::string sanitized;
base::RemoveChars(*current_dock_state, "\"", &sanitized);
dock_state_ = IsValidDockState(sanitized) ? sanitized : "right";
} else {
dock_state_ = "right";
}
}
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX)
auto* api_web_contents = api::WebContents::From(GetWebContents());
if (api_web_contents) {
auto* win =
static_cast<NativeWindowViews*>(api_web_contents->owner_window());
// When WCO is enabled, undock the devtools if the current dock
// position overlaps with the position of window controls to avoid
// broken layout.
if (win && win->IsWindowControlsOverlayEnabled()) {
if (IsAppRTL() && dock_state_ == "left") {
dock_state_ = "undocked";
} else if (dock_state_ == "right") {
dock_state_ = "undocked";
}
}
}
}
#endif
std::u16string javascript = base::UTF8ToUTF16(
"EUI.DockController.DockController.instance().setDockSide(\"" +
dock_state_ + "\");");
GetDevToolsWebContents()->GetPrimaryMainFrame()->ExecuteJavaScript(
javascript, base::NullCallback());
std::u16string javascript = base::UTF8ToUTF16(
"EUI.DockController.DockController.instance().setDockSide(\"" +
dock_state_ + "\");");
GetDevToolsWebContents()->GetPrimaryMainFrame()->ExecuteJavaScript(
javascript, base::NullCallback());
}
}
// If CloseDevTools was called re-entrantly during the show phase (e.g. from
// a JS devtools-focused handler), execute the deferred close now that the
// focus notification stack has fully unwound.
if (close_devtools_pending_) {
close_devtools_pending_ = false;
CloseDevTools();
return;
}
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)

View File

@@ -268,6 +268,14 @@ class InspectableWebContents
std::unique_ptr<InspectableWebContentsView> view_;
bool frontend_loaded_ = false;
// Re-entrancy guard: ShowDevTools triggers focus on the DevTools WebContents,
// which fires JS events whose microtask checkpoint can re-entrantly call
// CloseDevTools(). Destroying the WebContents or its widget while the focus
// notification is still iterating observers is a CHECK/UAF. These flags defer
// the close until the show path has fully unwound.
bool is_showing_devtools_ = false;
bool close_devtools_pending_ = false;
scoped_refptr<content::DevToolsAgentHost> agent_host_;
std::unique_ptr<content::DevToolsFrontendHost> frontend_host_;
std::unique_ptr<DevToolsEmbedderMessageDispatcher>

View File

@@ -725,7 +725,18 @@ v8::MaybeLocal<v8::Object> CreateProxyForAPI(
{
v8::Context::Scope inner_destination_context_scope(
destination_context);
proxy.Set(key, passed_value.ToLocalChecked());
// Use CreateDataProperty (not Set) so that a key named "__proto__"
// becomes an own data property instead of invoking the inherited
// Object.prototype.__proto__ setter and mutating the prototype.
v8::Local<v8::Value> proxied_value = passed_value.ToLocalChecked();
if (key->IsName()) {
std::ignore = proxy.GetHandle()->CreateDataProperty(
destination_context, key.As<v8::Name>(), proxied_value);
} else {
std::ignore = proxy.GetHandle()->CreateDataProperty(
destination_context, key.As<v8::Uint32>()->Value(),
proxied_value);
}
}
}
}

View File

@@ -350,7 +350,8 @@ ifdescribe(shouldRunCodesignTests)('autoUpdater behavior', function () {
});
});
it('should hit the download endpoint when an update is available and update successfully when the zip is provided even after a different update was staged', async () => {
it('should hit the download endpoint when an update is available and update successfully when the zip is provided even after a different update was staged', async function () {
this.timeout(180000);
await withUpdatableApp({
nextVersion: '2.0.0',
startFixture: 'update-stack',
@@ -478,7 +479,8 @@ ifdescribe(shouldRunCodesignTests)('autoUpdater behavior', function () {
});
});
it('should keep the update directory count bounded across repeated checks', async () => {
it('should keep the update directory count bounded across repeated checks', async function () {
this.timeout(240000);
// Verifies the orphan prune actually fires: after a second download
// completes and rewrites ShipItState.plist, the first directory is no
// longer referenced and must be removed when a third check begins.

View File

@@ -483,6 +483,52 @@ describe('contextBridge', () => {
expect(result).to.deep.equal([123, 456, 789, false]);
});
it('should not mutate the prototype when an object with an own __proto__ key is sent over the bridge', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', {
receive: (obj: any) => {
return [
Object.getPrototypeOf(obj) === Object.prototype,
obj.polluted,
Object.prototype.hasOwnProperty.call(obj, '__proto__'),
obj.data
];
}
});
});
const result = await callWithBindings((root: any) => {
const payload = Object.defineProperty({ data: 1 }, '__proto__', {
value: { polluted: true },
enumerable: true,
writable: true,
configurable: true
});
return root.example.receive(payload);
});
expect(result).to.deep.equal([true, undefined, true, 1]);
});
it('should not mutate the prototype when an object with an own __proto__ key is exposed', async () => {
await makeBindingWindow(() => {
const payload = Object.defineProperty({ data: 1 }, '__proto__', {
value: { polluted: true },
enumerable: true,
writable: true,
configurable: true
});
contextBridge.exposeInMainWorld('example', payload);
});
const result = await callWithBindings((root: any) => {
return [
Object.getPrototypeOf(root.example) === Object.prototype,
root.example.polluted,
Object.prototype.hasOwnProperty.call(root.example, '__proto__'),
root.example.data
];
});
expect(result).to.deep.equal([true, undefined, true, 1]);
});
it('it should proxy null', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', null);

View File

@@ -4,6 +4,7 @@ import { expect } from 'chai';
import * as http from 'node:http';
import { captureWithTabSourceId } from './lib/media-helpers';
import { ifit, listen } from './lib/spec-helpers';
import { closeAllWindows } from './lib/window-helpers';
@@ -417,6 +418,37 @@ describe('setDisplayMediaRequestHandler', () => {
expect(message).to.equal('Invalid state');
});
it('works when mediaDevices.getUserMedia uses a tab source id from webContents.getMediaSourceId', async () => {
const sourceWindow = new BrowserWindow({ show: false });
const requestingWindow = new BrowserWindow({ show: false });
await Promise.all([sourceWindow.loadURL(serverUrl), requestingWindow.loadURL(serverUrl)]);
const sourceId = sourceWindow.webContents.getMediaSourceId(requestingWindow.webContents);
const { ok, message, origin, videoTrackCount } = await captureWithTabSourceId(requestingWindow, sourceId);
expect(ok).to.be.true(message);
expect(origin).to.equal(new URL(serverUrl).origin);
expect(videoTrackCount).to.equal(1);
});
it('rejects a tab source id when used from a different requesting webContents', async () => {
const sourceWindow = new BrowserWindow({ show: false });
const registeredRequesterWindow = new BrowserWindow({ show: false });
const otherRequesterWindow = new BrowserWindow({ show: false });
await Promise.all([
sourceWindow.loadURL(serverUrl),
registeredRequesterWindow.loadURL(serverUrl),
otherRequesterWindow.loadURL(serverUrl)
]);
const sourceId = sourceWindow.webContents.getMediaSourceId(registeredRequesterWindow.webContents);
const { ok, message, origin } = await captureWithTabSourceId(otherRequesterWindow, sourceId);
expect(ok).to.be.false();
expect(message).to.match(/Invalid state|Error starting tab capture/);
expect(origin).to.equal(new URL(serverUrl).origin);
});
it('can remove a displayMediaRequestHandler', async () => {
const ses = session.fromPartition('' + Math.random());

View File

@@ -15,7 +15,7 @@ describe('Notification module', () => {
expect(Notification.isSupported()).to.be.a('boolean');
});
ifit(process.platform === 'darwin')('inits and gets id property', () => {
ifit(process.platform === 'darwin' || process.platform === 'win32')('inits and gets id property', () => {
const n = new Notification({
id: 'my-custom-id',
title: 'title',
@@ -25,7 +25,7 @@ describe('Notification module', () => {
expect(n.id).to.equal('my-custom-id');
});
ifit(process.platform === 'darwin')('id is read-only', () => {
ifit(process.platform === 'darwin' || process.platform === 'win32')('id is read-only', () => {
const n = new Notification({
id: 'my-custom-id',
title: 'title',
@@ -35,7 +35,7 @@ describe('Notification module', () => {
expect(() => { (n as any).id = 'new-id'; }).to.throw();
});
ifit(process.platform === 'darwin')('defaults id to a UUID when not provided', () => {
ifit(process.platform === 'darwin' || process.platform === 'win32')('defaults id to a UUID when not provided', () => {
const n = new Notification({
title: 'title',
body: 'body'
@@ -45,7 +45,7 @@ describe('Notification module', () => {
expect(n.id).to.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
});
ifit(process.platform === 'darwin')('defaults id to a UUID when empty string is provided', () => {
ifit(process.platform === 'darwin' || process.platform === 'win32')('defaults id to a UUID when empty string is provided', () => {
const n = new Notification({
id: '',
title: 'title',
@@ -55,7 +55,7 @@ describe('Notification module', () => {
expect(n.id).to.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
});
ifit(process.platform === 'darwin')('inits and gets groupId property', () => {
ifit(process.platform === 'darwin' || process.platform === 'win32')('inits and gets groupId property', () => {
const n = new Notification({
title: 'title',
body: 'body',
@@ -65,7 +65,7 @@ describe('Notification module', () => {
expect(n.groupId).to.equal('E017VKL2N8H|C07RBMNS9EK');
});
ifit(process.platform === 'darwin')('groupId is read-only', () => {
ifit(process.platform === 'darwin' || process.platform === 'win32')('groupId is read-only', () => {
const n = new Notification({
title: 'title',
body: 'body',
@@ -75,7 +75,7 @@ describe('Notification module', () => {
expect(() => { (n as any).groupId = 'new-group'; }).to.throw();
});
ifit(process.platform === 'darwin')('defaults groupId to empty string when not provided', () => {
ifit(process.platform === 'darwin' || process.platform === 'win32')('defaults groupId to empty string when not provided', () => {
const n = new Notification({
title: 'title',
body: 'body'
@@ -84,6 +84,82 @@ describe('Notification module', () => {
expect(n.groupId).to.equal('');
});
ifit(process.platform === 'win32')('inits and gets groupTitle property', () => {
const n = new Notification({
title: 'title',
body: 'body',
groupId: 'my-group',
groupTitle: 'My Group Title'
});
expect(n.groupTitle).to.equal('My Group Title');
});
ifit(process.platform === 'win32')('groupTitle is read-only', () => {
const n = new Notification({
title: 'title',
body: 'body',
groupId: 'my-group',
groupTitle: 'My Group Title'
});
expect(() => { (n as any).groupTitle = 'new-title'; }).to.throw();
});
ifit(process.platform === 'win32')('defaults groupTitle to empty string when not provided', () => {
const n = new Notification({
title: 'title',
body: 'body'
});
expect(n.groupTitle).to.equal('');
});
ifit(process.platform === 'win32')('throws when id exceeds 64 characters', () => {
expect(() => {
// eslint-disable-next-line no-new
new Notification({
id: 'a'.repeat(65),
title: 'title',
body: 'body'
});
}).to.throw(/id exceeds Windows limit of 64 UTF-16 characters/);
});
ifit(process.platform === 'win32')('throws when groupId exceeds 64 characters', () => {
expect(() => {
// eslint-disable-next-line no-new
new Notification({
groupId: 'a'.repeat(65),
title: 'title',
body: 'body'
});
}).to.throw(/groupId exceeds Windows limit of 64 UTF-16 characters/);
});
ifit(process.platform === 'win32')('throws when groupTitle is set without groupId', () => {
expect(() => {
// eslint-disable-next-line no-new
new Notification({
groupTitle: 'My Group',
title: 'title',
body: 'body'
});
}).to.throw(/groupTitle requires groupId to be set/);
});
ifit(process.platform === 'win32')('accepts id and groupId at 64 characters', () => {
const n = new Notification({
id: 'a'.repeat(64),
groupId: 'b'.repeat(64),
title: 'title',
body: 'body'
});
expect(n.id).to.equal('a'.repeat(64));
expect(n.groupId).to.equal('b'.repeat(64));
});
it('inits, gets and sets basic string properties correctly', () => {
const n = new Notification({
title: 'title',
@@ -249,6 +325,31 @@ describe('Notification module', () => {
}
});
ifit(process.platform === 'win32')('can show notification with custom id and groupId', () => {
const n = new Notification({
id: 'test-custom-id',
groupId: 'test-group',
title: 'test notification',
body: 'test body',
silent: true
});
n.show();
n.close();
});
ifit(process.platform === 'win32')('can show notification with groupId and groupTitle', () => {
const n = new Notification({
id: 'test-custom-id',
groupId: 'test-group',
groupTitle: 'Test Group Header',
title: 'test notification',
body: 'test body',
silent: true
});
n.show();
n.close();
});
ifit(process.platform === 'win32')('emits failed event', async () => {
const n = new Notification({
toastXml: 'not xml'

View File

@@ -12,6 +12,7 @@ import * as path from 'node:path';
import { setTimeout } from 'node:timers/promises';
import * as url from 'node:url';
import { captureWithTabSourceId } from './lib/media-helpers';
import { ifdescribe, defer, waitUntil, listen, ifit } from './lib/spec-helpers';
import { cleanupWebContents, closeAllWindows } from './lib/window-helpers';
@@ -1249,6 +1250,22 @@ describe('webContents module', () => {
await devtoolsOpened2;
expect(w.webContents.isDevToolsOpened()).to.be.true();
});
it('does not crash when closing DevTools immediately after opening', async () => {
const w = new BrowserWindow({ show: true });
await w.loadURL('about:blank');
const devToolsFocused = once(w.webContents, 'devtools-focused');
w.webContents.openDevTools({ mode: 'detach' });
w.webContents.inspectElement(100, 100);
await devToolsFocused;
const devtoolsClosed = once(w.webContents, 'devtools-closed');
w.webContents.closeDevTools();
await devtoolsClosed;
expect(w.webContents.isDevToolsOpened()).to.be.false();
});
});
describe('setDevToolsTitle() API', () => {
@@ -1781,9 +1798,33 @@ describe('webContents module', () => {
describe('getMediaSourceId()', () => {
afterEach(closeAllWindows);
it('returns a valid stream id', () => {
const w = new BrowserWindow({ show: false });
expect(w.webContents.getMediaSourceId(w.webContents)).to.be.a('string').that.is.not.empty();
let server: http.Server;
let serverUrl: string;
before(async () => {
server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end('');
});
serverUrl = (await listen(server)).url;
});
after(() => {
server.close();
});
it('returns a stream id that can be used by the registered requester', async () => {
const sourceWindow = new BrowserWindow({ show: false });
const requesterWindow = new BrowserWindow({ show: false });
await Promise.all([sourceWindow.loadURL(serverUrl), requesterWindow.loadURL(serverUrl)]);
const streamId = sourceWindow.webContents.getMediaSourceId(requesterWindow.webContents);
const { ok, message, origin, videoTrackCount } = await captureWithTabSourceId(requesterWindow, streamId);
expect(streamId).to.be.a('string').that.is.not.empty();
expect(ok, message).to.equal(true);
expect(origin).to.equal(new url.URL(serverUrl).origin);
expect(videoTrackCount).to.equal(1);
});
});

View File

@@ -46,7 +46,7 @@ if (feedUrl === 'remain-open') {
} else {
setTimeout(() => {
autoUpdater.checkForUpdates();
}, 1000);
}, 5000);
}
});

View File

@@ -0,0 +1,41 @@
const { app, BrowserWindow, webContents } = require('electron');
app.whenReady().then(() => {
const w = new BrowserWindow({
show: false,
webPreferences: {
webviewTag: true,
nodeIntegration: true,
contextIsolation: false
}
});
w.loadURL('data:text/html,' + encodeURIComponent(
'<webview id="wv" src="about:blank" style="width:100px;height:100px"></webview>' +
'<script>document.getElementById("wv").addEventListener("dom-ready", function() {' +
' document.title = "READY:" + this.getWebContentsId();' +
'});</script>'
));
const check = setInterval(() => {
if (!w.getTitle().startsWith('READY:')) return;
clearInterval(check);
const guestId = parseInt(w.getTitle().split(':')[1]);
const guest = webContents.fromId(guestId);
if (!guest) {
app.quit();
return;
}
// Destroying a guest webContents inside an event handler previously caused
// a use-after-free because Destroy() deleted the C++ object synchronously
// while the event emission was still on the call stack.
guest.on('did-navigate-in-page', () => {
guest.destroy();
});
guest.executeJavaScript('history.pushState({}, "", "#trigger")');
setTimeout(() => app.quit(), 2000);
}, 200);
});

39
spec/lib/media-helpers.ts Normal file
View File

@@ -0,0 +1,39 @@
import { BrowserWindow } from 'electron/main';
export interface TabSourceCaptureResult {
ok: boolean;
href: string;
message?: string;
origin: string;
videoTrackCount?: number;
}
export const captureWithTabSourceId = async (
requestingWindow: BrowserWindow,
streamId: string
): Promise<TabSourceCaptureResult> => {
return requestingWindow.webContents.executeJavaScript(`
navigator.mediaDevices.getUserMedia({
video: {
mandatory: {
chromeMediaSource: 'tab',
chromeMediaSourceId: ${JSON.stringify(streamId)},
},
},
}).then((stream) => {
const result = {
ok: stream instanceof MediaStream,
href: location.href,
origin: location.origin,
videoTrackCount: stream.getVideoTracks().length,
};
stream.getTracks().forEach((track) => track.stop());
return result;
}, (error) => ({
ok: false,
href: location.href,
message: error.message,
origin: location.origin,
}));
`);
};