mirror of
https://github.com/electron/electron.git
synced 2026-05-02 03:00:22 -04:00
Compare commits
20 Commits
v42.0.0-be
...
v42.0.0-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b38c88664e | ||
|
|
687bd0a1f0 | ||
|
|
9d194e28ab | ||
|
|
b6de8acc8a | ||
|
|
b51e62e560 | ||
|
|
f1958d838c | ||
|
|
8b2dba3726 | ||
|
|
4ac50292d5 | ||
|
|
81e76165ae | ||
|
|
acf615229d | ||
|
|
d5cea60ac7 | ||
|
|
60ec7cd0fb | ||
|
|
3d7d676bee | ||
|
|
c5b0ee8a9b | ||
|
|
3ea2c9c760 | ||
|
|
067fe3d1f1 | ||
|
|
75a7ebc7c0 | ||
|
|
64055f27e7 | ||
|
|
09156151c4 | ||
|
|
b07503d468 |
@@ -11,6 +11,7 @@
|
||||
"Bash(e patches:*)",
|
||||
"Bash(e sync:*)",
|
||||
"Skill(electron-chromium-upgrade)",
|
||||
"Skill(electron-node-upgrade)",
|
||||
"Read(*)",
|
||||
"Bash(echo:*)",
|
||||
"Bash(e build:*)",
|
||||
|
||||
2
.github/problem-matchers/clang.json
vendored
2
.github/problem-matchers/clang.json
vendored
@@ -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,
|
||||
|
||||
47
.github/siso-patches/0001-siso-reuse-the-outer-os.File-for-chunked-ReadAt-in-f.patch
vendored
Normal file
47
.github/siso-patches/0001-siso-reuse-the-outer-os.File-for-chunked-ReadAt-in-f.patch
vendored
Normal 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
|
||||
|
||||
22
.github/workflows/build.yml
vendored
22
.github/workflows/build.yml
vendored
@@ -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"
|
||||
|
||||
98
.github/workflows/pipeline-segment-build-siso-windows.yml
vendored
Normal file
98
.github/workflows/pipeline-segment-build-siso-windows.yml
vendored
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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') }}
|
||||
|
||||
14
.github/workflows/windows-publish.yml
vendored
14
.github/workflows/windows-publish.yml
vendored
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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&);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ if (feedUrl === 'remain-open') {
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
autoUpdater.checkForUpdates();
|
||||
}, 1000);
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
41
spec/fixtures/crash-cases/webview-destroy-in-event/index.js
vendored
Normal file
41
spec/fixtures/crash-cases/webview-destroy-in-event/index.js
vendored
Normal 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
39
spec/lib/media-helpers.ts
Normal 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,
|
||||
}));
|
||||
`);
|
||||
};
|
||||
Reference in New Issue
Block a user