Compare commits

..

19 Commits

Author SHA1 Message Date
Keeley Hammond
a229dbf7a5 chore: cherry-pick e045399a1ecb from chromium (#49791)
* chore: cherry-pick e045399a1ecb from chromium

* chore: update patch
2026-02-12 18:35:12 -08:00
John Kleinschmidt
cad033849b build: generate artifact attestions for released assets (#49769)
* build: generate artifact attestions for released assets (#48239)

* build: generate artifact attestions for released assets

* chore: address review feedback

---------

Co-authored-by: John Kleinschmidt <kleinschmidtorama@gmail.com>
(cherry picked from commit dec7f937ae)

* build: fixup attestation for release assets (#49732)

* build: fixup attestation for release assets

* Generate artifact attestation for generated artifacts

* set id-token for attestation

* Add artifact-metadata permission for attestation

* add permissions for testing attestations

* Revert "add permissions for testing attestations"

This reverts commit 0284bed175.

* Revert "set id-token for attestation"

This reverts commit 69a1b13a18.

* Revert "Generate artifact attestation for generated artifacts"

This reverts commit ee0536eceb.

(cherry picked from commit 0852893910)

* chore: update publish workflow

---------

Co-authored-by: Samuel Attard <sam@electronjs.org>
2026-02-12 10:51:35 -05:00
Keeley Hammond
16fc71f561 feat: add support for disclaiming utility processes (#49696)
feat: add support for disclaiming utility processes (#49128)

* feat: add support for disclaiming utility processes

* chore: update patches

---------
2026-02-12 10:32:09 -05:00
trop[bot]
f521b01eb8 build(dev-deps): update micromark-core-commonmark to 2.0.3 (#49760)
Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: David Sanders <dsanders11@ucsbalum.com>
2026-02-11 15:53:31 -05:00
trop[bot]
1a9c08914b ci: add markdownlint problem matcher (#49765)
Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: David Sanders <dsanders11@ucsbalum.com>
2026-02-11 13:58:04 -05:00
trop[bot]
0aba4a6ab8 fix: address upstream Chromium shift to enable CoreAudio Tap API for audio capture used in electron's desktopCapturer (🍏 macOS). (#49740)
* fix: Enable CoreAudio Tap API for Audio Capture (🍏 macOS)

Co-authored-by: Bug-Reaper <llama@soundsafari.io>

* fix: addressed markdown issue which caused pre-commit hook failure

🧑‍🔧 : Ref : https://github.com/electron/electron/pull/49717#issuecomment-3874660013

Co-authored-by: Bug-Reaper <llama@soundsafari.io>

* fix: updated docs to cover `desktopCapturer` nuances with macOS

📝 : PR https://github.com/electron/electron/pull/49717

Co-authored-by: Bug-Reaper <llama@soundsafari.io>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Bug-Reaper <llama@soundsafari.io>
2026-02-11 11:05:24 +01:00
trop[bot]
c49899af4c ci: fix patches changes detected in apply patches workflow (#49709)
Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: David Sanders <dsanders11@ucsbalum.com>
2026-02-06 21:29:56 -08:00
trop[bot]
92ef86b64a refactor: don't log error just for unsigned code (#49675)
Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Noah Gregory <noahmgregory@gmail.com>
2026-02-06 14:28:19 -08:00
Robo
30d8e1834c fix: Treat DND drop performed with NONE action as a cancellation (#49694)
Refs https://chromium-review.googlesource.com/c/chromium/src/+/7002773
2026-02-06 19:33:33 +09:00
trop[bot]
b35f4eeaf0 refactor: use ComPtr pattern for MSIX to avoid exception handling (#49688)
* Revert "fix: fix Windows MSIX release build errors (#49613)"

This reverts commit 4b5d5f9dd5.

Co-authored-by: Keeley Hammond <khammond@slack-corp.com>

* refactor: use WRL ComPtr pattern for MSIX to avoid exception handling

The MSIX auto-updater code was using C++/WinRT (winrt::* namespace), which requires exception handling (/EHsc). Mixing exception and non-exception handling code in the same binary is problematic at runtime. This commit refactors electron_api_msix_updater.cc to use an upstream Chromium pattern and eliminates the need for special exception handling build flags

Co-authored-by: Keeley Hammond <khammond@slack-corp.com>

* build: import correct packages

Co-authored-by: Keeley Hammond <khammond@slack-corp.com>

* build: consolidate IPackage declarations

Co-authored-by: Keeley Hammond <khammond@slack-corp.com>

* refactor: use IPackageManager/IPackageManager5/IPackageManager9 and IPackage/IPackage2/IPackage4/IPackage6 interfaces as needed for different API methods.

Also consolidates duplicate completion handler logic, fixes a bug in
RegisterRestartOnUpdate where the command line string could go out of
scope, and removes unused includes.

Co-authored-by: Keeley Hammond <khammond@slack-corp.com>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Keeley Hammond <khammond@slack-corp.com>
2026-02-05 14:37:04 -08:00
trop[bot]
63f7692da5 fix: menu state in macOS dock menus (#49626)
Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2026-02-05 10:57:47 -05:00
trop[bot]
a4af2354dc fix: default accelerator for role-based menu items (#49670)
fix: apply default accelerator for role-based menu items

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2026-02-05 10:57:01 -05:00
trop[bot]
bd49864f1d ci: use squash merge for apply patches workflow (#49674)
Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: David Sanders <dsanders11@ucsbalum.com>
2026-02-04 21:29:51 -08:00
trop[bot]
abc5d1280d fix(squirrel.mac): clean up old staged updates before downloading new update (#49637)
fix: clean up old staged updates before downloading new update

When checkForUpdates() is called while an update is already staged,
Squirrel creates a new temporary directory for the download without
cleaning up the old one. This can lead to disk usage growth when
new versions are released while the app hasn't restarted.

This adds a force parameter to pruneUpdateDirectories that bypasses
the AwaitingRelaunch state check. This is called before creating a
new temp directory, ensuring old staged updates are cleaned up.

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Andy Locascio <loc@anthropic.com>
2026-02-04 18:54:12 +01:00
trop[bot]
712bafde02 ci: handle PRs with no checks in rerun apply patches (#49663)
Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: David Sanders <dsanders11@ucsbalum.com>
2026-02-04 09:45:43 -08:00
trop[bot]
013768c429 docs: add Wayland note to win.getPosition() and win.getBounds() (#49660)
docs: add Wayland note to win.getPosition()

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2026-02-04 11:50:15 -05:00
trop[bot]
681a2c1aba fix: possible crash in FileSystem API (#49634)
Refs https://chromium-review.googlesource.com/6880247

Fixes a crash that can arise in the File System Access API in the
following scenario:

1. Create fileHandle1 at path1.
2. Call fileHandle1.remove() or user manually delete the file.
3. Create fileHandle2 at path2.
4. fileHandle2.move(path1).

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2026-02-04 16:12:59 +01:00
trop[bot]
98521d22ec fix: alt-space should route through 'system-context-menu' (#49641)
fix: alt-space should route through system-context-menu

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2026-02-04 16:11:59 +01:00
trop[bot]
faa40332ad fix: handle out of order recording errors in skia graphite (#49615)
* fix: handle out of order recording errors in skia graphite

Refs https://issues.chromium.org/issues/458722690

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

* chore: fixup patches

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: deepak1556 <hop2deep@gmail.com>
Co-authored-by: Niklas Wenzel <dev@nikwen.de>
2026-02-02 15:06:17 -08:00
50 changed files with 2130 additions and 349 deletions

View File

@@ -188,6 +188,7 @@ runs:
- name: Publish Electron Dist ${{ inputs.step-suffix }}
if: ${{ inputs.is-release == 'true' }}
shell: bash
id: github-upload
run: |
rm -rf src/out/Default/obj
cd src/electron
@@ -198,6 +199,11 @@ runs:
echo 'Uploading Electron release distribution to GitHub releases'
script/release/uploaders/upload.py --verbose
fi
- name: Generate artifact attestation
if: ${{ inputs.is-release == 'true' }}
uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0
with:
subject-path: ${{ steps.github-upload.outputs.UPLOADED_PATHS }}
- name: Generate siso report
if: ${{ inputs.target-platform != 'win' && !cancelled() }}
shell: bash

View File

@@ -0,0 +1,16 @@
{
"problemMatcher": [
{
"owner": "markdownlint",
"pattern": [
{
"regexp": "^(.+):(\\d+):(\\d+)\\s+(.*)$",
"file": 1,
"line": 2,
"column": 3,
"message": 4
}
]
}
]
}

View File

@@ -56,16 +56,17 @@ jobs:
path: src/electron
fetch-depth: 0
persist-credentials: false
ref: ${{ github.event.pull_request.head.sha }}
- name: Rebase onto Base Branch
ref: ${{ github.event.pull_request.base.ref }}
- name: Merge PR HEAD
working-directory: src/electron
env:
BASE_REF: ${{ github.event.pull_request.base.ref }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
git config user.email "electron@github.com"
git config user.name "Electron Bot"
git fetch origin ${BASE_REF}
git rebase origin/${BASE_REF}
git fetch origin refs/pull/${PR_NUMBER}/head
git merge --squash FETCH_HEAD
git commit -n -m "Squashed commits"
- name: Checkout & Sync & Save
uses: ./src/electron/.github/actions/checkout
with:

View File

@@ -43,9 +43,12 @@ jobs:
uses: ./src/electron/.github/actions/checkout
publish-x64:
uses: ./.github/workflows/pipeline-segment-electron-build.yml
uses: ./.github/workflows/pipeline-segment-electron-publish.yml
permissions:
artifact-metadata: write
attestations: write
contents: read
id-token: write
needs: checkout-linux
with:
environment: production-release
@@ -60,9 +63,12 @@ jobs:
secrets: inherit
publish-arm:
uses: ./.github/workflows/pipeline-segment-electron-build.yml
uses: ./.github/workflows/pipeline-segment-electron-publish.yml
permissions:
artifact-metadata: write
attestations: write
contents: read
id-token: write
needs: checkout-linux
with:
environment: production-release
@@ -77,9 +83,12 @@ jobs:
secrets: inherit
publish-arm64:
uses: ./.github/workflows/pipeline-segment-electron-build.yml
uses: ./.github/workflows/pipeline-segment-electron-publish.yml
permissions:
artifact-metadata: write
attestations: write
contents: read
id-token: write
needs: checkout-linux
with:
environment: production-release

View File

@@ -47,9 +47,12 @@ jobs:
target-platform: macos
publish-x64-darwin:
uses: ./.github/workflows/pipeline-segment-electron-build.yml
uses: ./.github/workflows/pipeline-segment-electron-publish.yml
permissions:
artifact-metadata: write
attestations: write
contents: read
id-token: write
needs: checkout-macos
with:
environment: production-release
@@ -64,9 +67,12 @@ jobs:
secrets: inherit
publish-x64-mas:
uses: ./.github/workflows/pipeline-segment-electron-build.yml
uses: ./.github/workflows/pipeline-segment-electron-publish.yml
permissions:
artifact-metadata: write
attestations: write
contents: read
id-token: write
needs: checkout-macos
with:
environment: production-release
@@ -81,9 +87,12 @@ jobs:
secrets: inherit
publish-arm64-darwin:
uses: ./.github/workflows/pipeline-segment-electron-build.yml
uses: ./.github/workflows/pipeline-segment-electron-publish.yml
permissions:
artifact-metadata: write
attestations: write
contents: read
id-token: write
needs: checkout-macos
with:
environment: production-release
@@ -98,9 +107,12 @@ jobs:
secrets: inherit
publish-arm64-mas:
uses: ./.github/workflows/pipeline-segment-electron-build.yml
uses: ./.github/workflows/pipeline-segment-electron-publish.yml
permissions:
artifact-metadata: write
attestations: write
contents: read
id-token: write
needs: checkout-macos
with:
environment: production-release

View File

@@ -65,9 +65,11 @@ jobs:
curl -sL "https://chromium.googlesource.com/chromium/src/+/${chromium_revision}/buildtools/DEPS?format=TEXT" | base64 -d > src/buildtools/DEPS
gclient sync --spec="solutions=[{'name':'src/buildtools','url':None,'deps_file':'DEPS','custom_vars':{'process_deps':True},'managed':False}]"
- name: Add ESLint problem matcher
- name: Add problem matchers
shell: bash
run: echo "::add-matcher::src/electron/.github/problem-matchers/eslint-stylish.json"
run: |
echo "::add-matcher::src/electron/.github/problem-matchers/eslint-stylish.json"
echo "::add-matcher::src/electron/.github/problem-matchers/markdownlint.json"
- name: Run Lint
shell: bash
run: |
@@ -85,4 +87,8 @@ jobs:
run: |
cd src/electron
node script/yarn.js tsc -p tsconfig.script.json
- name: Check GHA Workflows
shell: bash
run: |
cd src/electron
node script/copy-pipeline-segment-publish.js --check

View File

@@ -0,0 +1,237 @@
# AUTOGENERATED FILE - DO NOT EDIT MANUALLY
# ONLY EDIT .github/workflows/pipeline-segment-electron-build.yml
name: Pipeline Segment - Electron Build
on:
workflow_call:
inputs:
environment:
description: using the production or testing environment
required: false
type: string
target-platform:
type: string
description: Platform to run on, can be macos, win or linux
required: true
target-arch:
type: string
description: Arch to build for, can be x64, arm64, ia32 or arm
required: true
target-variant:
type: string
description: Variant to build for, no effect on non-macOS target platforms. Can
be darwin, mas or all.
default: all
build-runs-on:
type: string
description: What host to run the build
required: true
build-container:
type: string
description: JSON container information for aks runs-on
required: false
default: '{"image":null}'
is-release:
description: Whether this build job is a release job
required: true
type: boolean
default: false
gn-build-type:
description: The gn build type - testing or release
required: true
type: string
default: testing
generate-symbols:
description: Whether or not to generate symbols
required: true
type: boolean
default: false
upload-to-storage:
description: Whether or not to upload build artifacts to external storage
required: true
type: string
default: "0"
is-asan:
description: Building the Address Sanitizer (ASan) Linux build
required: false
type: boolean
default: false
enable-ssh:
description: Enable SSH debugging
required: false
type: boolean
default: false
permissions: {}
concurrency:
group: electron-build-${{ inputs.target-platform }}-${{ inputs.target-arch
}}-${{ inputs.target-variant }}-${{ inputs.is-asan }}-${{
github.ref_protected == true && github.run_id || github.ref }}
cancel-in-progress: ${{ github.ref_protected != true }}
env:
CHROMIUM_GIT_COOKIE: ${{ secrets.CHROMIUM_GIT_COOKIE }}
CHROMIUM_GIT_COOKIE_WINDOWS_STRING: ${{ secrets.CHROMIUM_GIT_COOKIE_WINDOWS_STRING }}
DD_API_KEY: ${{ secrets.DD_API_KEY }}
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 }}
jobs:
build:
defaults:
run:
shell: bash
runs-on: ${{ inputs.build-runs-on }}
permissions:
artifact-metadata: write
attestations: write
contents: read
id-token: write
container: ${{ fromJSON(inputs.build-container) }}
environment: ${{ inputs.environment }}
env:
TARGET_ARCH: ${{ inputs.target-arch }}
TARGET_PLATFORM: ${{ inputs.target-platform }}
steps:
- name: Create src dir
run: |
mkdir src
- name: Checkout Electron
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
path: src/electron
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- name: Setup SSH Debugging
if: ${{ inputs.target-platform == 'macos' && (inputs.enable-ssh ||
env.ACTIONS_STEP_DEBUG == 'true') }}
uses: ./src/electron/.github/actions/ssh-debug
with:
tunnel: "true"
env:
CLOUDFLARE_TUNNEL_CERT: ${{ secrets.CLOUDFLARE_TUNNEL_CERT }}
CLOUDFLARE_TUNNEL_HOSTNAME: ${{ vars.CLOUDFLARE_TUNNEL_HOSTNAME }}
CLOUDFLARE_USER_CA_CERT: ${{ secrets.CLOUDFLARE_USER_CA_CERT }}
AUTHORIZED_USERS: ${{ secrets.SSH_DEBUG_AUTHORIZED_USERS }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Free up space (macOS)
if: ${{ inputs.target-platform == 'macos' }}
uses: ./src/electron/.github/actions/free-space-macos
- name: Check disk space after freeing up space
if: ${{ inputs.target-platform == 'macos' }}
run: df -h
- name: Setup Node.js/npm
if: ${{ inputs.target-platform == 'macos' }}
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 20.19.x
cache: yarn
cache-dependency-path: src/electron/yarn.lock
- name: Install Dependencies
uses: ./src/electron/.github/actions/install-dependencies
- name: Install AZCopy
if: ${{ inputs.target-platform == 'macos' }}
run: brew install azcopy
- name: Set GN_EXTRA_ARGS for Linux
if: ${{ inputs.target-platform == 'linux' }}
run: >
if [ "${{ inputs.target-arch }}" = "arm" ]; then
if [ "${{ inputs.is-release }}" = true ]; then
GN_EXTRA_ARGS='target_cpu="arm" build_tflite_with_xnnpack=false symbol_level=1'
else
GN_EXTRA_ARGS='target_cpu="arm" build_tflite_with_xnnpack=false'
fi
elif [ "${{ inputs.target-arch }}" = "arm64" ]; then
GN_EXTRA_ARGS='target_cpu="arm64" fatal_linker_warnings=false enable_linux_installer=false'
elif [ "${{ inputs.is-asan }}" = true ]; then
GN_EXTRA_ARGS='is_asan=true'
fi
echo "GN_EXTRA_ARGS=$GN_EXTRA_ARGS" >> $GITHUB_ENV
- name: Set Chromium Git Cookie
uses: ./src/electron/.github/actions/set-chromium-cookie
- name: Install Build Tools
uses: ./src/electron/.github/actions/install-build-tools
- name: Generate DEPS Hash
run: |
node src/electron/script/generate-deps-hash.js
DEPSHASH=v1-src-cache-$(cat src/electron/.depshash)
echo "DEPSHASH=$DEPSHASH" >> $GITHUB_ENV
echo "CACHE_PATH=$DEPSHASH.tar" >> $GITHUB_ENV
- name: Restore src cache via AZCopy
if: ${{ inputs.target-platform != 'linux' }}
uses: ./src/electron/.github/actions/restore-cache-azcopy
with:
target-platform: ${{ inputs.target-platform }}
- name: Restore src cache via AKS
if: ${{ inputs.target-platform == 'linux' }}
uses: ./src/electron/.github/actions/restore-cache-aks
- name: Checkout Electron
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
path: src/electron
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- name: Fix Sync
if: ${{ inputs.target-platform != 'linux' }}
uses: ./src/electron/.github/actions/fix-sync
with:
target-platform: ${{ inputs.target-platform }}
env:
ELECTRON_DEPOT_TOOLS_DISABLE_LOG: true
- name: Init Build Tools
run: >
e init -f --root=$(pwd) --out=Default ${{ inputs.gn-build-type }}
--import ${{ inputs.gn-build-type }} --target-cpu ${{
inputs.target-arch }} --remote-build siso
- name: Run Electron Only Hooks
run: |
e d gclient runhooks --spec="solutions=[{'name':'src/electron','url':None,'deps_file':'DEPS','custom_vars':{'process_deps':False},'managed':False}]"
- name: Regenerate DEPS Hash
run: >
(cd src/electron && git checkout .) && node
src/electron/script/generate-deps-hash.js
echo "DEPSHASH=$(cat src/electron/.depshash)" >> $GITHUB_ENV
- name: Add CHROMIUM_BUILDTOOLS_PATH to env
run: echo "CHROMIUM_BUILDTOOLS_PATH=$(pwd)/src/buildtools" >> $GITHUB_ENV
- name: Free up space (macOS)
if: ${{ inputs.target-platform == 'macos' }}
uses: ./src/electron/.github/actions/free-space-macos
- name: Build Electron
if: ${{ inputs.target-platform != 'macos' || (inputs.target-variant == 'all' ||
inputs.target-variant == 'darwin') }}
uses: ./src/electron/.github/actions/build-electron
with:
target-arch: ${{ inputs.target-arch }}
target-platform: ${{ inputs.target-platform }}
artifact-platform: ${{ inputs.target-platform == 'macos' && 'darwin' ||
inputs.target-platform }}
is-release: ${{ inputs.is-release }}
generate-symbols: ${{ inputs.generate-symbols }}
upload-to-storage: ${{ inputs.upload-to-storage }}
is-asan: ${{ inputs.is-asan }}
- name: Set GN_EXTRA_ARGS for MAS Build
if: ${{ inputs.target-platform == 'macos' && (inputs.target-variant == 'all' ||
inputs.target-variant == 'mas') }}
run: |
echo "MAS_BUILD=true" >> $GITHUB_ENV
GN_EXTRA_ARGS='is_mas_build=true'
echo "GN_EXTRA_ARGS=$GN_EXTRA_ARGS" >> $GITHUB_ENV
- name: Build Electron (MAS)
if: ${{ inputs.target-platform == 'macos' && (inputs.target-variant == 'all' ||
inputs.target-variant == 'mas') }}
uses: ./src/electron/.github/actions/build-electron
with:
target-arch: ${{ inputs.target-arch }}
target-platform: ${{ inputs.target-platform }}
artifact-platform: mas
is-release: ${{ inputs.is-release }}
generate-symbols: ${{ inputs.generate-symbols }}
upload-to-storage: ${{ inputs.upload-to-storage }}
step-suffix: (mas)

View File

@@ -35,20 +35,20 @@ jobs:
echo "Processing PR #${PR_NUMBER}"
# Find the Apply Patches workflow check for this PR
CHECK=$(gh pr checks "$PR_NUMBER" --json link,name,state,workflow --jq '[.[] | select(.workflow == "Apply Patches" and .name == "apply-patches")] | first')
CHECK=$(gh pr view "$PR_NUMBER" --json statusCheckRollup --jq '[.statusCheckRollup[] | select(.workflowName == "Apply Patches" and .name == "apply-patches")] | first')
if [ -z "$CHECK" ] || [ "$CHECK" = "null" ]; then
echo " No Apply Patches workflow found for PR #${PR_NUMBER}"
continue
fi
STATE=$(echo "$CHECK" | jq -r '.state')
if [ "$STATE" = "SKIPPED" ]; then
CONCLUSION=$(echo "$CHECK" | jq -r '.conclusion')
if [ "$CONCLUSION" = "SKIPPED" ]; then
echo " apply-patches job was skipped for PR #${PR_NUMBER} (no patches)"
continue
fi
LINK=$(echo "$CHECK" | jq -r '.link')
LINK=$(echo "$CHECK" | jq -r '.detailsUrl')
# Extract the run ID from the link (format: .../runs/RUN_ID/job/JOB_ID)
RUN_ID=$(echo "$LINK" | grep -oE 'runs/[0-9]+' | cut -d'/' -f2)

View File

@@ -51,9 +51,12 @@ jobs:
target-platform: win
publish-x64-win:
uses: ./.github/workflows/pipeline-segment-electron-build.yml
uses: ./.github/workflows/pipeline-segment-electron-publish.yml
permissions:
artifact-metadata: write
attestations: write
contents: read
id-token: write
needs: checkout-windows
with:
environment: production-release
@@ -67,9 +70,12 @@ jobs:
secrets: inherit
publish-arm64-win:
uses: ./.github/workflows/pipeline-segment-electron-build.yml
uses: ./.github/workflows/pipeline-segment-electron-publish.yml
permissions:
artifact-metadata: write
attestations: write
contents: read
id-token: write
needs: checkout-windows
with:
environment: production-release
@@ -83,9 +89,12 @@ jobs:
secrets: inherit
publish-x86-win:
uses: ./.github/workflows/pipeline-segment-electron-build.yml
uses: ./.github/workflows/pipeline-segment-electron-publish.yml
permissions:
artifact-metadata: write
attestations: write
contents: read
id-token: write
needs: checkout-windows
with:
environment: production-release

View File

@@ -420,37 +420,6 @@ action("electron_generate_node_defines") {
args = [ rebase_path(target_gen_dir) ] + rebase_path(inputs)
}
# MSIX updater needs to be in a separate source_set because it uses C++/WinRT
# headers that require exceptions to be enabled.
source_set("electron_msix_updater") {
sources = [
"shell/browser/api/electron_api_msix_updater.cc",
"shell/browser/api/electron_api_msix_updater.h",
]
configs += [ "//third_party/electron_node:node_external_config" ]
public_configs = [ ":electron_lib_config" ]
if (is_win) {
cflags_cc = [
"/EHsc", # Enable C++ exceptions for C++/WinRT
"-Wno-c++98-compat-extra-semi", #Suppress C++98 compatibility warnings
]
include_dirs = [ "//third_party/nearby/src/internal/platform/implementation/windows/generated" ]
}
deps = [
"//base",
"//content/public/browser",
"//gin",
"//third_party/electron_node/deps/simdjson",
"//third_party/electron_node/deps/uv",
"//v8",
]
}
source_set("electron_lib") {
configs += [
"//v8:external_startup_data",
@@ -466,7 +435,6 @@ source_set("electron_lib") {
":electron_fuses",
":electron_generate_node_defines",
":electron_js2c",
":electron_msix_updater",
":electron_version_header",
":resources",
"buildflags",

View File

@@ -756,6 +756,9 @@ Returns [`Rectangle`](structures/rectangle.md) - The `bounds` of the window as `
> [!NOTE]
> On macOS, the y-coordinate value returned will be at minimum the [Tray](tray.md) height. For example, calling `win.setBounds({ x: 25, y: 20, width: 800, height: 600 })` with a tray height of 38 means that `win.getBounds()` will return `{ x: 25, y: 38, width: 800, height: 600 }`.
> [!NOTE]
> On Wayland, this method will return `{ x: 0, y: 0, ... }` as introspecting or programmatically changing the global window coordinates is prohibited.
#### `win.getBackgroundColor()`
Returns `string` - Gets the background color of the window in Hex (`#RRGGBB`) format.
@@ -969,6 +972,9 @@ Moves window to `x` and `y`.
Returns `Integer[]` - Contains the window's current position.
> [!NOTE]
> On Wayland, this method will return `[0, 0]` as introspecting or programmatically changing the global window coordinates is prohibited.
#### `win.setTitle(title)`
* `title` string

View File

@@ -862,6 +862,9 @@ Returns [`Rectangle`](structures/rectangle.md) - The `bounds` of the window as `
> [!NOTE]
> On macOS, the y-coordinate value returned will be at minimum the [Tray](tray.md) height. For example, calling `win.setBounds({ x: 25, y: 20, width: 800, height: 600 })` with a tray height of 38 means that `win.getBounds()` will return `{ x: 25, y: 38, width: 800, height: 600 }`.
> [!NOTE]
> On Wayland, this method will return `{ x: 0, y: 0, ... }` as introspecting or programmatically changing the global window coordinates is prohibited.
#### `win.getBackgroundColor()`
Returns `string` - Gets the background color of the window in Hex (`#RRGGBB`) format.
@@ -1087,6 +1090,9 @@ Not supported on Wayland (Linux).
Returns `Integer[]` - Contains the window's current position.
> [!NOTE]
> On Wayland, this method will return `[0, 0]` as introspecting or programmatically changing the global window coordinates is prohibited.
#### `win.setTitle(title)`
* `title` string

View File

@@ -94,18 +94,45 @@ The `desktopCapturer` module has the following methods:
Returns `Promise<DesktopCapturerSource[]>` - Resolves with an array of [`DesktopCapturerSource`](structures/desktop-capturer-source.md) objects, each `DesktopCapturerSource` represents a screen or an individual window that can be captured.
> [!NOTE]
> Capturing the screen contents requires user consent on macOS 10.15 Catalina or higher,
> which can detected by [`systemPreferences.getMediaAccessStatus`][].
> * Capturing audio requires `NSAudioCaptureUsageDescription` Info.plist key on macOS 14.2 Sonoma and higher - [read more](#macos-versions-142-or-higher).
> * Capturing the screen contents requires user consent on macOS 10.15 Catalina or higher, which can detected by [`systemPreferences.getMediaAccessStatus`][].
[`navigator.mediaDevices.getUserMedia`]: https://developer.mozilla.org/en/docs/Web/API/MediaDevices/getUserMedia
[`systemPreferences.getMediaAccessStatus`]: system-preferences.md#systempreferencesgetmediaaccessstatusmediatype-windows-macos
## Caveats
### Linux
`desktopCapturer.getSources(options)` only returns a single source on Linux when using Pipewire.
PipeWire supports a single capture for both screens and windows. If you request the window and screen type, the selected source will be returned as a window capture.
`navigator.mediaDevices.getUserMedia` does not work on macOS for audio capture due to a fundamental limitation whereby apps that want to access the system's audio require a [signed kernel extension](https://developer.apple.com/library/archive/documentation/Security/Conceptual/System_Integrity_Protection_Guide/KernelExtensions/KernelExtensions.html). Chromium, and by extension Electron, does not provide this.
---
It is possible to circumvent this limitation by capturing system audio with another macOS app like Soundflower and passing it through a virtual audio input device. This virtual device can then be queried with `navigator.mediaDevices.getUserMedia`.
### MacOS versions 14.2 or higher
`NSAudioCaptureUsageDescription` Info.plist key must be added in-order for audio to be captured by `desktopCapturer`. If instead you are running electron from another program like a terminal or IDE then that parent program must contain the Info.plist key.
This is in order to facillitate use of Apple's new [CoreAudio Tap API](https://developer.apple.com/documentation/CoreAudio/capturing-system-audio-with-core-audio-taps#Configure-the-sample-code-project) by Chromium.
> [!WARNING]
> Failure of `desktopCapturer` to start an audio stream due to `NSAudioCaptureUsageDescription` permission not present will still create a dead audio stream however no warnings or errors are displayed.
As of electron `v39.0.0-beta.4` Chromium [made Apple's new `CoreAudio Tap API` the default](https://source.chromium.org/chromium/chromium/src/+/ad17e8f8b93d5f34891b06085d373a668918255e) for desktop audio capture. There is no fallback to the older `Screen & System Audio Recording` permissions system even if [CoreAudio Tap API](https://developer.apple.com/documentation/CoreAudio/capturing-system-audio-with-core-audio-taps) stream creation fails.
If you need to continue using `Screen & System Audio Recording` permissions for `desktopCapturer` on macOS versions 14.2 and later, you can apply a chromium feature flag to force use of that older permissions system:
```js
// main.js (right beneath your require/import statments)
app.commandLine.appendSwitch('disable-features', 'MacCatapLoopbackAudioForScreenShare')
```
---
### MacOS versions 12.7.6 or lower
`navigator.mediaDevices.getUserMedia` does not work on macOS versions 12.7.6 and prior for audio capture due to a fundamental limitation whereby apps that want to access the system's audio require a [signed kernel extension](https://developer.apple.com/library/archive/documentation/Security/Conceptual/System_Integrity_Protection_Guide/KernelExtensions/KernelExtensions.html). Chromium, and by extension Electron, does not provide this. Only in macOS 13 and onwards does Apple provide APIs to capture desktop audio without the need for a signed kernel extension.
It is possible to circumvent this limitation by capturing system audio with another macOS app like [BlackHole](https://existential.audio/blackhole/) or [Soundflower](https://rogueamoeba.com/freebies/soundflower/) and passing it through a virtual audio input device. This virtual device can then be queried with `navigator.mediaDevices.getUserMedia`.

View File

@@ -107,7 +107,7 @@ A `string` (optional) indicating the item's role, if set. Can be `undo`, `redo`,
#### `menuItem.accelerator`
An `Accelerator` (optional) indicating the item's accelerator, if set.
An `Accelerator | null` indicating the item's accelerator, if set.
#### `menuItem.userAccelerator` _Readonly_ _macOS_

View File

@@ -36,6 +36,12 @@ Process: [Main](../glossary.md#main-process)<br />
`com.apple.security.cs.allow-unsigned-executable-memory` entitlements. This will allow the utility process
to load unsigned libraries. Unless you specifically need this capability, it is best to leave this disabled.
Default is `false`.
* `disclaim` boolean (optional) _macOS_ - With this flag, the utility process will disclaim
responsibility for the child process. This causes the operating system to consider the child
process as a separate entity for purposes of security policies like Transparency, Consent, and
Control (TCC). When responsibility is disclaimed, the parent process will not be attributed
for any TCC requests initiated by the child process. This is useful when launching processes
that run third-party or otherwise untrusted code. Default is `false`.
* `respondToAuthRequestsFromMainProcess` boolean (optional) - With this flag, all HTTP 401 and 407 network
requests created via the [net module](net.md) will allow responding to them via the
[`app#login`](app.md#event-login) event in the main process instead of the default

View File

@@ -37,6 +37,22 @@ webContents.setWindowOpenHandler((details) => {
})
```
### Behavior Changed: `NSAudioCaptureUsageDescription` should be included in your app's Info.plist file to use `desktopCapturer` (🍏 macOS ≥14.2)
Per [Chromium update](https://source.chromium.org/chromium/chromium/src/+/ad17e8f8b93d5f34891b06085d373a668918255e) which enables Apple's newer [CoreAudio Tap API](https://developer.apple.com/documentation/CoreAudio/capturing-system-audio-with-core-audio-taps#Configure-the-sample-code-project) by default, you now must have `NSAudioCaptureUsageDescription` defined in your `Info.plist` to use `desktopCapturer`.
Electron's `desktopCapturer` will create a dead audio stream if the new permission is absent however no errors or warnings will occur. This is partially a side-effect of Chromium not falling back to the older `Screen & System Audio Recording` permissions system if the new system fails.
To restore previous behavior:
```js
// main.js (right beneath your require/import statments)
app.commandLine.appendSwitch(
'disable-features',
'MacCatapLoopbackAudioForScreenShare'
)
```
### Behavior Changed: shared texture OSR `paint` event data structure
When using shared texture offscreen rendering feature, the `paint` event now emits a more structured object.

View File

@@ -200,7 +200,7 @@ macOS has a number of platform-specific menu roles available. Many of these map
* `recentDocuments` - The submenu is an "Open Recent" menu.
* `clearRecentDocuments` - Map to the [`clearRecentDocuments`](https://developer.apple.com/documentation/appkit/nsdocumentcontroller/clearrecentdocuments(_:)) action.
* `shareMenu` - The submenu is [share menu][ShareMenu]. The `sharingItem` property must also be set to indicate the item to share.
* `shareMenu` - The submenu is [share menu](../api/share-menu.md). The `sharingItem` property must also be set to indicate the item to share.
> [!IMPORTANT]
> When specifying a `role` on macOS, `label` and `accelerator` are the only

View File

@@ -277,6 +277,8 @@ filenames = {
"shell/browser/api/electron_api_in_app_purchase.h",
"shell/browser/api/electron_api_menu.cc",
"shell/browser/api/electron_api_menu.h",
"shell/browser/api/electron_api_msix_updater.cc",
"shell/browser/api/electron_api_msix_updater.h",
"shell/browser/api/electron_api_native_theme.cc",
"shell/browser/api/electron_api_native_theme.h",
"shell/browser/api/electron_api_net_log.cc",

View File

@@ -353,6 +353,7 @@ export function shouldOverrideCheckStatus (role: RoleId) {
export function getDefaultAccelerator (role: RoleId) {
if (hasRole(role)) return roleList[role].accelerator;
return undefined;
}
export function shouldRegisterAccelerator (role: RoleId) {

View File

@@ -25,7 +25,7 @@ const MenuItem = function (this: any, options: any) {
this.overrideReadOnlyProperty('type', roles.getDefaultType(this.role));
this.overrideReadOnlyProperty('role');
this.overrideReadOnlyProperty('accelerator');
this.overrideReadOnlyProperty('accelerator', roles.getDefaultAccelerator(this.role));
this.overrideReadOnlyProperty('icon');
this.overrideReadOnlyProperty('submenu');

View File

@@ -57,7 +57,8 @@
"url": "^0.11.4",
"webpack": "^5.95.0",
"webpack-cli": "^5.1.4",
"wrapper-webpack-plugin": "^2.2.0"
"wrapper-webpack-plugin": "^2.2.0",
"yaml": "^2.8.1"
},
"private": true,
"scripts": {
@@ -132,6 +133,10 @@
"DEPS": [
"node script/gen-hunspell-filenames.js",
"node script/gen-libc++-filenames.js"
],
".github/workflows/pipeline-segment-electron-build.yml": [
"node script/copy-pipeline-segment-publish.js",
"git add .github/workflows/pipeline-segment-electron-publish.yml"
]
},
"resolutions": {

View File

@@ -147,3 +147,6 @@ viz_fix_visual_artifacts_due_to_resizing_root_render_pass_with_dcomp.patch
viz_do_not_overallocate_surface_on_initial_render.patch
viz_create_isbufferqueuesupportedandenabled.patch
viz_fix_visual_artifacts_while_resizing_window_with_dcomp.patch
graphite_handle_out_of_order_recording_errors.patch
ozone_wayland_treat_dnd_drop_performed_with_none_action_as_a.patch
cherry-pick-e045399a1ecb.patch

View File

@@ -0,0 +1,132 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dominik=20R=C3=B6ttsches?= <drott@chromium.org>
Date: Thu, 12 Feb 2026 06:35:36 -0800
Subject: Avoid stale iteration in CSSFontFeatureValuesMap
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
To avoid invalid iterator state, take a snapshot of the
map when creating the iteration source. This addresses
the immediate problem of iterating while modifying.
Remaining work tracked in https://crbug.com/483936078
Fixed: 483569511
Change-Id: Ie29cfdf7ed94bbe189b44c842a5efce571bb2cee
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7566570
Commit-Queue: Dominik Röttsches <drott@chromium.org>
Reviewed-by: Anders Hartvoll Ruud <andruud@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1583927}
diff --git a/third_party/blink/renderer/core/css/css_font_feature_values_map.cc b/third_party/blink/renderer/core/css/css_font_feature_values_map.cc
index 0c5990799fbfdff5f1d1e04a9038a471217ad0d2..2ea27901e3ba503e7e1acc5dacf90dc60d52ac1a 100644
--- a/third_party/blink/renderer/core/css/css_font_feature_values_map.cc
+++ b/third_party/blink/renderer/core/css/css_font_feature_values_map.cc
@@ -13,16 +13,15 @@ class FontFeatureValuesMapIterationSource final
: public PairSyncIterable<CSSFontFeatureValuesMap>::IterationSource {
public:
FontFeatureValuesMapIterationSource(const CSSFontFeatureValuesMap& map,
- const FontFeatureAliases* aliases)
- : map_(map), aliases_(aliases), iterator_(aliases->begin()) {}
+ const FontFeatureAliases aliases)
+ : map_(map),
+ aliases_(std::move(aliases)),
+ iterator_(aliases_.begin()) {}
bool FetchNextItem(ScriptState* script_state,
String& map_key,
Vector<uint32_t>& map_value) override {
- if (!aliases_) {
- return false;
- }
- if (iterator_ == aliases_->end()) {
+ if (iterator_ == aliases_.end()) {
return false;
}
map_key = iterator_->key;
@@ -37,9 +36,13 @@ class FontFeatureValuesMapIterationSource final
}
private:
- // Needs to be kept alive while we're iterating over it.
const Member<const CSSFontFeatureValuesMap> map_;
- const FontFeatureAliases* aliases_;
+ // Create a copy to keep the iterator from becoming invalid if there are
+ // modifications to the aliases HashMap while iterating.
+ // TODO(https://crbug.com/483936078): Implement live/stable iteration over
+ // FontFeatureAliases by changing its storage type, avoiding taking a copy
+ // here.
+ const FontFeatureAliases aliases_;
FontFeatureAliases::const_iterator iterator_;
};
@@ -49,8 +52,8 @@ uint32_t CSSFontFeatureValuesMap::size() const {
PairSyncIterable<CSSFontFeatureValuesMap>::IterationSource*
CSSFontFeatureValuesMap::CreateIterationSource(ScriptState*) {
- return MakeGarbageCollected<FontFeatureValuesMapIterationSource>(*this,
- aliases_);
+ return MakeGarbageCollected<FontFeatureValuesMapIterationSource>(
+ *this, aliases_ ? *aliases_ : FontFeatureAliases());
}
bool CSSFontFeatureValuesMap::GetMapEntry(ScriptState*,
diff --git a/third_party/blink/web_tests/external/wpt/css/css-fonts/font_feature_values_map_iteration.html b/third_party/blink/web_tests/external/wpt/css/css-fonts/font_feature_values_map_iteration.html
new file mode 100644
index 0000000000000000000000000000000000000000..eac7198b0b4a58007cbcc77ad3e9357a1009117c
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-fonts/font_feature_values_map_iteration.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>CSSFontFeatureValuesMap Iteration and Modification</title>
+ <link
+ rel="help"
+ href="https://drafts.csswg.org/css-fonts-4/#om-fontfeaturevalues"
+ />
+ <meta
+ name="assert"
+ content="Iteration while modifying CSSFontFeatureValuesMap does not crash."
+ />
+ <script type="text/javascript" src="/resources/testharness.js"></script>
+ <script
+ type="text/javascript"
+ src="/resources/testharnessreport.js"
+ ></script>
+ </head>
+ <body>
+ <style>
+ @font-feature-values TestFont {
+ @styleset {
+ a: 1;
+ b: 2;
+ c: 3;
+ }
+ }
+ </style>
+ <script>
+ test(() => {
+ const rule = document.styleSheets[0].cssRules[0];
+ const map = rule.styleset;
+ const iterator = map.entries();
+ let count = 0;
+
+ while (count < 10) {
+ const { value: entry, done } = iterator.next();
+ if (done) break;
+
+ const [key, value] = entry;
+
+ map.delete(key);
+ for (let i = 0; i < 100; i++) {
+ map.set(`newkey_${count}_${i}`, i);
+ }
+
+ count++;
+ }
+ }, "Iteration of the CSSFontFeatureValuesMap does not crash.");
+ </script>
+ </body>
+</html>

View File

@@ -9,6 +9,8 @@ Subject: feat: configure launch options for service process
Allows configuring base::LaunchOptions::handles_to_inherit, base::LaunchOptions::stdout_handle,
base::LaunchOptions::stderr_handle and base::LaunchOptions::feedback_cursor_off when launching
the child process.
- Mac:
Allows configuring base::LaunchOptions::disclaim_responsibility when launching the child process.
- All:
Allows configuring base::LauncOptions::current_directory, base::LaunchOptions::enviroment
and base::LaunchOptions::clear_environment.
@@ -165,10 +167,10 @@ index 0791b5317fc6846389f65f93734ae5e816d04623..48948b409d6da58ade72c60ed848df49
FinishStartSandboxedProcessOnLauncherThread,
this));
diff --git a/content/browser/service_host/service_process_host_impl.cc b/content/browser/service_host/service_process_host_impl.cc
index d9c14f91747bde0e76056d7f2f2ada166e67f994..53be16879777a3b9bef58ead5f7e420c1bf6acbe 100644
index d9c14f91747bde0e76056d7f2f2ada166e67f994..09335acac17f526fb8d8e42e4b2d993b11045786 100644
--- a/content/browser/service_host/service_process_host_impl.cc
+++ b/content/browser/service_host/service_process_host_impl.cc
@@ -69,6 +69,17 @@ void LaunchServiceProcess(mojo::GenericPendingReceiver receiver,
@@ -69,6 +69,21 @@ void LaunchServiceProcess(mojo::GenericPendingReceiver receiver,
utility_options.WithGpuClientAllowed();
}
@@ -179,6 +181,10 @@ index d9c14f91747bde0e76056d7f2f2ada166e67f994..53be16879777a3b9bef58ead5f7e420c
+#elif BUILDFLAG(IS_POSIX)
+ utility_options.WithAdditionalFds(std::move(service_options.fds_to_remap));
+#endif
+#if BUILDFLAG(IS_MAC)
+ utility_options.WithDisclaimResponsibility(
+ service_options.disclaim_responsibility);
+#endif
+ utility_options.WithCurrentDirectory(service_options.current_directory);
+ utility_options.WithEnvironment(service_options.environment,
+ service_options.clear_environment);
@@ -187,7 +193,7 @@ index d9c14f91747bde0e76056d7f2f2ada166e67f994..53be16879777a3b9bef58ead5f7e420c
UtilityProcessHost::Start(std::move(utility_options),
diff --git a/content/browser/service_host/utility_process_host.cc b/content/browser/service_host/utility_process_host.cc
index ab48c8b88a7cf311205b221169308d96e95f239f..d714d2022f685052e1def3b0d242d2ea596d790b 100644
index ab48c8b88a7cf311205b221169308d96e95f239f..54883f202c1769227c823d46454173b53692a7dd 100644
--- a/content/browser/service_host/utility_process_host.cc
+++ b/content/browser/service_host/utility_process_host.cc
@@ -245,13 +245,13 @@ UtilityProcessHost::Options& UtilityProcessHost::Options::WithFileToPreload(
@@ -207,7 +213,7 @@ index ab48c8b88a7cf311205b221169308d96e95f239f..d714d2022f685052e1def3b0d242d2ea
#if BUILDFLAG(USE_ZYGOTE)
UtilityProcessHost::Options& UtilityProcessHost::Options::WithZygoteForTesting(
@@ -261,6 +261,36 @@ UtilityProcessHost::Options& UtilityProcessHost::Options::WithZygoteForTesting(
@@ -261,6 +261,45 @@ UtilityProcessHost::Options& UtilityProcessHost::Options::WithZygoteForTesting(
}
#endif // BUILDFLAG(USE_ZYGOTE)
@@ -240,11 +246,20 @@ index ab48c8b88a7cf311205b221169308d96e95f239f..d714d2022f685052e1def3b0d242d2ea
+ return *this;
+}
+#endif // BUILDFLAG(IS_WIN)
+
+#if BUILDFLAG(IS_MAC)
+UtilityProcessHost::Options&
+UtilityProcessHost::Options::WithDisclaimResponsibility(
+ bool disclaim_responsibility) {
+ disclaim_responsibility_ = disclaim_responsibility;
+ return *this;
+}
+#endif // BUILDFLAG(IS_MAC)
+
UtilityProcessHost::Options&
UtilityProcessHost::Options::WithBoundReceiverOnChildProcessForTesting(
mojo::GenericPendingReceiver receiver) {
@@ -525,9 +555,26 @@ bool UtilityProcessHost::StartProcess() {
@@ -525,9 +564,30 @@ bool UtilityProcessHost::StartProcess() {
}
#endif // BUILDFLAG(ENABLE_GPU_CHANNEL_MEDIA_CAPTURE) && !BUILDFLAG(IS_WIN)
@@ -269,11 +284,15 @@ index ab48c8b88a7cf311205b221169308d96e95f239f..d714d2022f685052e1def3b0d242d2ea
+#if BUILDFLAG(IS_WIN)
+ delegate->SetFeedbackCursorOff(options_.feedback_cursor_off_);
+#endif // BUILDFLAG(IS_WIN)
+
+#if BUILDFLAG(IS_MAC)
+ delegate->SetDisclaimResponsibility(options_.disclaim_responsibility_);
+#endif // BUILDFLAG(IS_MAC)
#if BUILDFLAG(IS_WIN)
if (!options_.preload_libraries_.empty()) {
diff --git a/content/browser/service_host/utility_process_host.h b/content/browser/service_host/utility_process_host.h
index 580fa663e729a43bef44a10de8983c4aecc312fb..f39af3df87786a472f987309ac0dea699b9f0d9f 100644
index 580fa663e729a43bef44a10de8983c4aecc312fb..c339f2b8504beee6ed2e5746a8e6b961a7aa9746 100644
--- a/content/browser/service_host/utility_process_host.h
+++ b/content/browser/service_host/utility_process_host.h
@@ -31,6 +31,7 @@
@@ -284,7 +303,7 @@ index 580fa663e729a43bef44a10de8983c4aecc312fb..f39af3df87786a472f987309ac0dea69
#endif // BUILDFLAG(IS_WIN)
namespace base {
@@ -134,14 +135,31 @@ class CONTENT_EXPORT UtilityProcessHost final
@@ -134,14 +135,36 @@ class CONTENT_EXPORT UtilityProcessHost final
std::variant<base::FilePath, base::ScopedFD> file);
#endif
@@ -315,11 +334,16 @@ index 580fa663e729a43bef44a10de8983c4aecc312fb..f39af3df87786a472f987309ac0dea69
+ // Specifies if the process should trigger mouse cursor feedback.
+ Options& WithFeedbackCursorOff(bool feedback_cursor_off);
+#endif // BUILDFLAG(IS_WIN)
+
+#if BUILDFLAG(IS_MAC)
+ // Specifies if the process should disclaim TCC responsibility.
+ Options& WithDisclaimResponsibility(bool disclaim_responsibility);
+#endif // BUILDFLAG(IS_MAC)
+
// Requests that the process bind a receiving pipe targeting the interface
// named by `receiver`. Calls to this method generally end up in
// `ChildThreadImpl::OnBindReceiver()` and the option is used for testing
@@ -185,6 +203,27 @@ class CONTENT_EXPORT UtilityProcessHost final
@@ -185,6 +208,32 @@ class CONTENT_EXPORT UtilityProcessHost final
std::optional<raw_ptr<ZygoteCommunication>> zygote_for_testing_;
#endif // BUILDFLAG(USE_ZYGOTE)
@@ -343,12 +367,17 @@ index 580fa663e729a43bef44a10de8983c4aecc312fb..f39af3df87786a472f987309ac0dea69
+ // Specifies if the process should trigger mouse cursor feedback.
+ bool feedback_cursor_off_ = false;
+#endif // BUILDFLAG(IS_WIN)
+
+#if BUILDFLAG(IS_MAC)
+ // Specifies if the process should disclaim TCC responsibility.
+ bool disclaim_responsibility_ = false;
+#endif // BUILDFLAG(IS_MAC)
+
#if BUILDFLAG(ENABLE_GPU_CHANNEL_MEDIA_CAPTURE)
// Whether or not to bind viz::mojom::Gpu to the utility process.
bool allowed_gpu_;
diff --git a/content/browser/service_host/utility_sandbox_delegate.cc b/content/browser/service_host/utility_sandbox_delegate.cc
index 8f89c28144e1ecf3d7dbf9a3b43031cbad12a8ea..faa49bb63bd2e9080da441286bdbf427f22cd26f 100644
index 8f89c28144e1ecf3d7dbf9a3b43031cbad12a8ea..0c6e724f8ad0926076274f696b77b17b77f2aaa9 100644
--- a/content/browser/service_host/utility_sandbox_delegate.cc
+++ b/content/browser/service_host/utility_sandbox_delegate.cc
@@ -43,17 +43,19 @@ UtilitySandboxedProcessLauncherDelegate::
@@ -406,8 +435,24 @@ index 8f89c28144e1ecf3d7dbf9a3b43031cbad12a8ea..faa49bb63bd2e9080da441286bdbf427
#if BUILDFLAG(USE_ZYGOTE)
ZygoteCommunication* UtilitySandboxedProcessLauncherDelegate::GetZygote() {
@@ -203,6 +222,15 @@ UtilitySandboxedProcessLauncherDelegate::GetProcessRequirement() {
return std::nullopt;
}
+
+void UtilitySandboxedProcessLauncherDelegate::SetDisclaimResponsibility(
+ bool disclaim_responsibility) {
+ disclaim_responsibility_ = disclaim_responsibility;
+}
+
+bool UtilitySandboxedProcessLauncherDelegate::DisclaimResponsibility() {
+ return disclaim_responsibility_;
+}
#endif // BUILDFLAG(IS_MAC)
} // namespace content
diff --git a/content/browser/service_host/utility_sandbox_delegate.h b/content/browser/service_host/utility_sandbox_delegate.h
index f2e8c1d62c1cb1677f618b584ed401685b03034b..7c47ec471e4676365cf3e023808983cb3e7a18d0 100644
index f2e8c1d62c1cb1677f618b584ed401685b03034b..00a20885092bd299e817685dd18c3971b25f721b 100644
--- a/content/browser/service_host/utility_sandbox_delegate.h
+++ b/content/browser/service_host/utility_sandbox_delegate.h
@@ -36,7 +36,9 @@ class CONTENT_EXPORT UtilitySandboxedProcessLauncherDelegate
@@ -438,7 +483,12 @@ index f2e8c1d62c1cb1677f618b584ed401685b03034b..7c47ec471e4676365cf3e023808983cb
#if BUILDFLAG(USE_ZYGOTE)
void SetZygote(ZygoteCommunication* handle);
@@ -77,9 +84,7 @@ class CONTENT_EXPORT UtilitySandboxedProcessLauncherDelegate
@@ -74,12 +81,12 @@ class CONTENT_EXPORT UtilitySandboxedProcessLauncherDelegate
#if BUILDFLAG(IS_MAC)
std::optional<base::mac::ProcessRequirement> GetProcessRequirement() override;
+ void SetDisclaimResponsibility(bool disclaim_responsibility);
+ bool DisclaimResponsibility() override;
#endif // BUILDFLAG(IS_MAC)
private:
@@ -448,7 +498,7 @@ index f2e8c1d62c1cb1677f618b584ed401685b03034b..7c47ec471e4676365cf3e023808983cb
#if BUILDFLAG(IS_WIN)
// Adds preload-libraries to the delegate blob for utility_main() to access
@@ -95,12 +100,17 @@ class CONTENT_EXPORT UtilitySandboxedProcessLauncherDelegate
@@ -95,12 +102,20 @@ class CONTENT_EXPORT UtilitySandboxedProcessLauncherDelegate
std::optional<raw_ptr<ZygoteCommunication>> zygote_;
#endif // BUILDFLAG(USE_ZYGOTE)
@@ -463,6 +513,9 @@ index f2e8c1d62c1cb1677f618b584ed401685b03034b..7c47ec471e4676365cf3e023808983cb
+#if BUILDFLAG(IS_WIN)
+ bool feedback_cursor_off_ = false;
+#endif // BUILDFLAG(IS_WIN)
+#if BUILDFLAG(IS_MAC)
+ bool disclaim_responsibility_ = false;
+#endif // BUILDFLAG(IS_MAC)
};
} // namespace content
@@ -489,10 +542,10 @@ index 39c96d4423b24695eee86353057cfeed19318b57..31b343d97b7672294644041c9bb1a4cd
}
diff --git a/content/public/browser/service_process_host.cc b/content/public/browser/service_process_host.cc
index d1bc550a891979e2d41d8d5b18a2f9287468e460..5fcac7a8493e5065f80303067a04f59e7c4509ef 100644
index d1bc550a891979e2d41d8d5b18a2f9287468e460..5d255f628788bc8b40d8df0039b08c06ffec8730 100644
--- a/content/public/browser/service_process_host.cc
+++ b/content/public/browser/service_process_host.cc
@@ -53,12 +53,53 @@ ServiceProcessHost::Options::WithExtraCommandLineSwitches(
@@ -53,12 +53,62 @@ ServiceProcessHost::Options::WithExtraCommandLineSwitches(
return *this;
}
@@ -542,12 +595,21 @@ index d1bc550a891979e2d41d8d5b18a2f9287468e460..5fcac7a8493e5065f80303067a04f59e
+ return *this;
+}
+#endif // #if BUILDFLAG(IS_WIN)
+
+#if BUILDFLAG(IS_MAC)
+ServiceProcessHost::Options&
+ServiceProcessHost::Options::WithDisclaimResponsibility(
+ bool should_disclaim_responsibility) {
+ disclaim_responsibility = should_disclaim_responsibility;
+ return *this;
+}
+#endif // BUILDFLAG(IS_MAC)
+
#if BUILDFLAG(IS_WIN)
ServiceProcessHost::Options&
ServiceProcessHost::Options::WithPreloadedLibraries(
diff --git a/content/public/browser/service_process_host.h b/content/public/browser/service_process_host.h
index 0062d2cb6634b8b29977a0312516b1b13936b40a..611a52e908f4cb70fbe5628e220a082e45320b70 100644
index 0062d2cb6634b8b29977a0312516b1b13936b40a..888ff36d70c83010f1f45e9eeb2dd6b573158db5 100644
--- a/content/public/browser/service_process_host.h
+++ b/content/public/browser/service_process_host.h
@@ -14,6 +14,7 @@
@@ -569,7 +631,7 @@ index 0062d2cb6634b8b29977a0312516b1b13936b40a..611a52e908f4cb70fbe5628e220a082e
namespace base {
class Process;
} // namespace base
@@ -94,11 +99,35 @@ class CONTENT_EXPORT ServiceProcessHost {
@@ -94,11 +99,40 @@ class CONTENT_EXPORT ServiceProcessHost {
// Specifies extra command line switches to append before launch.
Options& WithExtraCommandLineSwitches(std::vector<std::string> switches);
@@ -601,11 +663,16 @@ index 0062d2cb6634b8b29977a0312516b1b13936b40a..611a52e908f4cb70fbe5628e220a082e
+ // Specifies if the process should trigger mouse cursor feedback.
+ Options& WithFeedbackCursorOff(bool feedback_cursor_off);
+#endif // #if BUILDFLAG(IS_WIN)
+
+#if BUILDFLAG(IS_MAC)
+ // Specifies if the process should disclaim TCC responsibility.
+ Options& WithDisclaimResponsibility(bool disclaim_responsibility);
+#endif // BUILDFLAG(IS_MAC)
+
#if BUILDFLAG(IS_WIN)
// Specifies libraries to preload before the sandbox is locked down. Paths
// should be absolute paths. Libraries will be preloaded before sandbox
@@ -127,11 +156,23 @@ class CONTENT_EXPORT ServiceProcessHost {
@@ -127,11 +161,26 @@ class CONTENT_EXPORT ServiceProcessHost {
std::optional<GURL> site;
std::optional<int> child_flags;
std::vector<std::string> extra_switches;
@@ -626,6 +693,9 @@ index 0062d2cb6634b8b29977a0312516b1b13936b40a..611a52e908f4cb70fbe5628e220a082e
+#if BUILDFLAG(IS_WIN)
+ bool feedback_cursor_off = false;
+#endif // BUILDFLAG(IS_WIN)
+#if BUILDFLAG(IS_MAC)
+ bool disclaim_responsibility = false;
+#endif // BUILDFLAG(IS_MAC)
};
// An interface which can be implemented and registered/unregistered with

View File

@@ -84,10 +84,10 @@ index 2648adb1cf38ab557b66ffd0e3034b26b04d76d6..98eab587f343f6ca472efc3d4e7b31b2
private:
const std::string service_interface_name_;
diff --git a/content/browser/service_host/utility_process_host.cc b/content/browser/service_host/utility_process_host.cc
index d714d2022f685052e1def3b0d242d2ea596d790b..fb4cee1416836c4d0b807a862507a52347f24dd2 100644
index 54883f202c1769227c823d46454173b53692a7dd..b2a2f64435f566c67531bac9d0782215739d8fe6 100644
--- a/content/browser/service_host/utility_process_host.cc
+++ b/content/browser/service_host/utility_process_host.cc
@@ -629,7 +629,7 @@ void UtilityProcessHost::OnProcessCrashed(int exit_code) {
@@ -642,7 +642,7 @@ void UtilityProcessHost::OnProcessCrashed(int exit_code) {
: Client::CrashType::kPreIpcInitialization;
}
#endif // BUILDFLAG(IS_WIN)
@@ -97,7 +97,7 @@ index d714d2022f685052e1def3b0d242d2ea596d790b..fb4cee1416836c4d0b807a862507a523
std::optional<std::string> UtilityProcessHost::GetServiceName() {
diff --git a/content/browser/service_host/utility_process_host.h b/content/browser/service_host/utility_process_host.h
index f39af3df87786a472f987309ac0dea699b9f0d9f..6f470d19fa5fa0bd4d2bdb1be4aa0cb3a70d3936 100644
index c339f2b8504beee6ed2e5746a8e6b961a7aa9746..e75b67e08755c90670d44b1e06a685aa2b35539d 100644
--- a/content/browser/service_host/utility_process_host.h
+++ b/content/browser/service_host/utility_process_host.h
@@ -88,7 +88,7 @@ class CONTENT_EXPORT UtilityProcessHost final

View File

@@ -0,0 +1,348 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Sunny Sachanandani <sunnyps@chromium.org>
Date: Fri, 30 Jan 2026 12:51:05 -0800
Subject: [graphite] Handle out of order recording errors
Explicitly handle out of order recording errors to crash just like async
shader compile failed errors. Also, emit the insert status to an UMA
histogram at 1% subsampling and log all error statuses at runtime.
Bug: 458722690
Change-Id: Id94657be6ae870dcf8adba71aff216386a6a6964
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7533855
Commit-Queue: Sunny Sachanandani <sunnyps@chromium.org>
Auto-Submit: Sunny Sachanandani <sunnyps@chromium.org>
Reviewed-by: Jonathan Ross <jonross@chromium.org>
Reviewed-by: Michael Ludwig <michaelludwig@google.com>
Cr-Commit-Position: refs/heads/main@{#1577487}
diff --git a/gpu/command_buffer/service/graphite_shared_context.cc b/gpu/command_buffer/service/graphite_shared_context.cc
index f9a47ac16bddc2352d81f43629d8ce8c2d2b3b99..9856d68f893e4329fda5d002835613161b346bc1 100644
--- a/gpu/command_buffer/service/graphite_shared_context.cc
+++ b/gpu/command_buffer/service/graphite_shared_context.cc
@@ -4,7 +4,10 @@
#include "gpu/command_buffer/service/graphite_shared_context.h"
+#include "base/logging.h"
#include "base/memory/ptr_util.h"
+#include "base/metrics/histogram_macros.h"
+#include "base/rand_util.h"
#include "base/task/single_thread_task_runner.h"
#include "gpu/command_buffer/common/shm_count.h"
#include "third_party/skia/include/core/SkColorSpace.h"
@@ -14,6 +17,44 @@
namespace gpu {
namespace {
+// This is emitted to UMA - values should not be reordered, only appended!
+// LINT.IfChange(InsertRecordingStatusUma)
+enum class InsertRecordingStatusUma {
+ kSuccess,
+ kInvalidRecording,
+ kPromiseImageInstantiationFailed,
+ kAddCommandsFailed,
+ kAsyncShaderCompilesFailed,
+ kOutOfOrderRecording,
+ kMaxValue = kOutOfOrderRecording
+};
+// LINT.ThenChange(//tools/metrics/histograms/metadata/gpu/enums.xml:GraphiteInsertRecordingStatus)
+
+InsertRecordingStatusUma InsertRecordingStatusUma(
+ skgpu::graphite::InsertStatus insert_status) {
+ // InsertStatus almost behaves like an enum class, but not quite since it can
+ // convert to both bool and integer types and can't be used in a switch.
+ if (insert_status == skgpu::graphite::InsertStatus::kSuccess) {
+ return InsertRecordingStatusUma::kSuccess;
+ } else if (insert_status ==
+ skgpu::graphite::InsertStatus::kInvalidRecording) {
+ return InsertRecordingStatusUma::kInvalidRecording;
+ } else if (insert_status ==
+ skgpu::graphite::InsertStatus::kPromiseImageInstantiationFailed) {
+ return InsertRecordingStatusUma::kPromiseImageInstantiationFailed;
+ } else if (insert_status ==
+ skgpu::graphite::InsertStatus::kAddCommandsFailed) {
+ return InsertRecordingStatusUma::kAddCommandsFailed;
+ } else if (insert_status ==
+ skgpu::graphite::InsertStatus::kAsyncShaderCompilesFailed) {
+ return InsertRecordingStatusUma::kAsyncShaderCompilesFailed;
+ } else if (insert_status ==
+ skgpu::graphite::InsertStatus::kOutOfOrderRecording) {
+ return InsertRecordingStatusUma::kOutOfOrderRecording;
+ }
+ NOTREACHED();
+}
+
struct RecordingContext {
skgpu::graphite::GpuFinishedProc old_finished_proc;
skgpu::graphite::GpuFinishedContext old_context;
@@ -216,20 +257,38 @@ bool GraphiteSharedContext::InsertRecordingImpl(
auto insert_status = graphite_context_->insertRecording(*info_ptr);
- // TODO(433845560): Check the kAddCommandsFailed failures.
- // Crash only if we're not simulating a failure for testing.
const bool simulating_insert_failure =
info_ptr->fSimulatedStatus != skgpu::graphite::InsertStatus::kSuccess;
- // InsertStatus::kAsyncShaderCompilesFailed is also an unrecoverable error for
- // which we should also clear the disk shader cache in case the error was due
- // to a corrupted cached shader blob.
+ // Crash, log, or emit UMA only if we're not simulating a failure for testing.
+ if (!simulating_insert_failure) {
+ if (base::ShouldRecordSubsampledMetric(0.01)) {
+ UMA_HISTOGRAM_ENUMERATION("GPU.Graphite.InsertRecordingStatus",
+ InsertRecordingStatusUma(insert_status));
+ }
+ if (insert_status != skgpu::graphite::InsertStatus::kSuccess) {
+ // skgpu::graphite::InsertStatus almost behaves like an enum class, but
+ // not quite - it can't be static_cast to an int.
+ LOG(ERROR) << "Graphite insertRecording failed with status "
+ << static_cast<int>(InsertRecordingStatusUma(insert_status));
+ }
+ }
+
+ // kAsyncShaderCompilesFailed and kOutOfOrderRecording are unrecoverable
+ // failures because they cause future recordings to be rendered incorrectly.
+ // TODO(433845560): Check the kAddCommandsFailed failures.
+ const bool is_unrecoverable_failure =
+ insert_status ==
+ skgpu::graphite::InsertStatus::kAsyncShaderCompilesFailed ||
+ insert_status == skgpu::graphite::InsertStatus::kOutOfOrderRecording;
+ // For kAsyncShaderCompilesFailed, we should also clear the disk shader
+ // cache in case the error was due to a corrupted cached shader blob.
+ std::optional<GpuProcessShmCount::ScopedIncrement> use_shader_cache;
if (insert_status ==
skgpu::graphite::InsertStatus::kAsyncShaderCompilesFailed) {
- GpuProcessShmCount::ScopedIncrement use_shader_cache(
- use_shader_cache_shm_count_);
- CHECK(simulating_insert_failure);
+ use_shader_cache.emplace(use_shader_cache_shm_count_);
}
+ CHECK(simulating_insert_failure || !is_unrecoverable_failure);
// All other failure modes are recoverable in the sense that future recordings
// will be rendered correctly, so merely return a boolean here so that callers
diff --git a/gpu/command_buffer/service/graphite_shared_context_unittest.cc b/gpu/command_buffer/service/graphite_shared_context_unittest.cc
index 0de107a9d86ff8217a4c537598f198f313ecf9df..852343ab6804efed962b707b748ddf1920c20ea7 100644
--- a/gpu/command_buffer/service/graphite_shared_context_unittest.cc
+++ b/gpu/command_buffer/service/graphite_shared_context_unittest.cc
@@ -6,6 +6,7 @@
#include "base/threading/thread.h"
#include "gpu/command_buffer/common/shm_count.h"
+#include "gpu/command_buffer/service/skia_utils.h"
#include "skia/buildflags.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -86,8 +87,7 @@ class GraphiteSharedContextTest : public testing::TestWithParam<bool> {
wgpu::DeviceDescriptor device_desc = {};
- wgpu::Device device =
- wgpu::Adapter(adapters[0].Get()).CreateDevice(&device_desc);
+ auto device = wgpu::Adapter(adapters[0].Get()).CreateDevice(&device_desc);
CHECK(device);
skgpu::graphite::DawnBackendContext backend_context = {};
@@ -95,7 +95,11 @@ class GraphiteSharedContextTest : public testing::TestWithParam<bool> {
backend_context.fDevice = device;
backend_context.fQueue = device.GetQueue();
- skgpu::graphite::ContextOptions context_options = {};
+ // Use the default Graphite context options that Chromium uses e.g. disallow
+ // things like out of order recordings.
+ gpu::GpuDriverBugWorkarounds workarounds;
+ auto context_options = GetDefaultGraphiteContextOptions(workarounds);
+
graphite_shared_context_ = std::make_unique<GraphiteSharedContext>(
skgpu::graphite::ContextFactory::MakeDawn(backend_context,
context_options),
@@ -133,14 +137,12 @@ TEST_P(GraphiteSharedContextTest, ConcurrentAccess) {
auto run_graphite_functions =
[](GraphiteSharedContext* graphite_shared_context) {
// Call a method that acquires the lock
- std::unique_ptr<skgpu::graphite::Recorder> recorder =
- graphite_shared_context->makeRecorder();
+ auto recorder = graphite_shared_context->makeRecorder();
EXPECT_TRUE(recorder);
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 10; ++j) {
- std::unique_ptr<skgpu::graphite::Recording> recording =
- recorder->snap();
+ auto recording = recorder->snap();
skgpu::graphite::InsertRecordingInfo info = {};
info.fRecording = recording.get();
EXPECT_TRUE(recording);
@@ -172,18 +174,17 @@ TEST_P(GraphiteSharedContextTest, ConcurrentAccess) {
}
TEST_P(GraphiteSharedContextTest, AsyncShaderCompilesFailed) {
- std::unique_ptr<skgpu::graphite::Recorder> recorder =
- graphite_shared_context_->makeRecorder();
+ auto recorder = graphite_shared_context_->makeRecorder();
EXPECT_TRUE(recorder);
auto ii = SkImageInfo::Make(64, 64, kN32_SkColorType, kPremul_SkAlphaType);
- sk_sp<SkSurface> surface1 = SkSurfaces::RenderTarget(recorder.get(), ii);
+ auto surface1 = SkSurfaces::RenderTarget(recorder.get(), ii);
surface1->getCanvas()->clear(SK_ColorRED);
- sk_sp<SkSurface> surface2 = SkSurfaces::RenderTarget(recorder.get(), ii);
+ auto surface2 = SkSurfaces::RenderTarget(recorder.get(), ii);
surface2->getCanvas()->drawImage(surface1->makeTemporaryImage(), 0, 0);
- std::unique_ptr<skgpu::graphite::Recording> recording = recorder->snap();
+ auto recording = recorder->snap();
EXPECT_TRUE(recording);
skgpu::graphite::InsertRecordingInfo info = {};
@@ -197,19 +198,43 @@ TEST_P(GraphiteSharedContextTest, AsyncShaderCompilesFailed) {
EXPECT_FALSE(graphite_shared_context_->insertRecording(info));
}
+TEST_P(GraphiteSharedContextTest, OutOfOrderRecording) {
+ auto recorder = graphite_shared_context_->makeRecorder();
+ EXPECT_TRUE(recorder);
+
+ auto ii = SkImageInfo::Make(64, 64, kN32_SkColorType, kPremul_SkAlphaType);
+ auto surface1 = SkSurfaces::RenderTarget(recorder.get(), ii);
+ surface1->getCanvas()->clear(SK_ColorRED);
+ auto recording1 = recorder->snap();
+ EXPECT_TRUE(recording1);
+
+ auto surface2 = SkSurfaces::RenderTarget(recorder.get(), ii);
+ surface2->getCanvas()->drawImage(surface1->makeTemporaryImage(), 0, 0);
+ auto recording2 = recorder->snap();
+ EXPECT_TRUE(recording2);
+
+ skgpu::graphite::InsertRecordingInfo info = {};
+
+ info.fRecording = recording2.get();
+ graphite_shared_context_->insertRecording(info);
+
+ info.fRecording = recording1.get();
+ EXPECT_DEATH_IF_SUPPORTED(graphite_shared_context_->insertRecording(info),
+ "");
+}
+
TEST_P(GraphiteSharedContextTest, AddCommandsFailed) {
- std::unique_ptr<skgpu::graphite::Recorder> recorder =
- graphite_shared_context_->makeRecorder();
+ auto recorder = graphite_shared_context_->makeRecorder();
EXPECT_TRUE(recorder);
auto ii = SkImageInfo::Make(64, 64, kN32_SkColorType, kPremul_SkAlphaType);
- sk_sp<SkSurface> surface1 = SkSurfaces::RenderTarget(recorder.get(), ii);
+ auto surface1 = SkSurfaces::RenderTarget(recorder.get(), ii);
surface1->getCanvas()->clear(SK_ColorRED);
- sk_sp<SkSurface> surface2 = SkSurfaces::RenderTarget(recorder.get(), ii);
+ auto surface2 = SkSurfaces::RenderTarget(recorder.get(), ii);
surface2->getCanvas()->drawImage(surface1->makeTemporaryImage(), 0, 0);
- std::unique_ptr<skgpu::graphite::Recording> recording = recorder->snap();
+ auto recording = recorder->snap();
EXPECT_TRUE(recording);
skgpu::graphite::InsertRecordingInfo info = {};
@@ -223,39 +248,37 @@ TEST_P(GraphiteSharedContextTest, AddCommandsFailed) {
}
TEST_P(GraphiteSharedContextTest, LowPendingRecordings) {
- std::unique_ptr<skgpu::graphite::Recorder> recorder =
- graphite_shared_context_->makeRecorder();
+ auto recorder = graphite_shared_context_->makeRecorder();
EXPECT_TRUE(recorder);
- std::unique_ptr<skgpu::graphite::Recording> recording = recorder->snap();
- EXPECT_TRUE(recording);
-
- skgpu::graphite::InsertRecordingInfo info = {};
- info.fRecording = recording.get();
-
// No flush is expected if the number of pending recordings is low.
EXPECT_CALL(backend_flush_callback_, Flush()).Times(0);
for (size_t i = 0; i < kMaxPendingRecordings - 1; ++i) {
+ auto recording = recorder->snap();
+ EXPECT_TRUE(recording);
+
+ skgpu::graphite::InsertRecordingInfo info = {};
+ info.fRecording = recording.get();
+
EXPECT_TRUE(graphite_shared_context_->insertRecording(info));
}
}
TEST_P(GraphiteSharedContextTest, MaxPendingRecordings) {
- std::unique_ptr<skgpu::graphite::Recorder> recorder =
- graphite_shared_context_->makeRecorder();
+ auto recorder = graphite_shared_context_->makeRecorder();
EXPECT_TRUE(recorder);
- std::unique_ptr<skgpu::graphite::Recording> recording = recorder->snap();
- EXPECT_TRUE(recording);
-
- skgpu::graphite::InsertRecordingInfo info = {};
- info.fRecording = recording.get();
-
// Expect a flush when the number of pending recordings reaches the max.
EXPECT_CALL(backend_flush_callback_, Flush()).Times(1);
for (size_t i = 0; i < kMaxPendingRecordings; ++i) {
+ auto recording = recorder->snap();
+ EXPECT_TRUE(recording);
+
+ skgpu::graphite::InsertRecordingInfo info = {};
+ info.fRecording = recording.get();
+
EXPECT_TRUE(graphite_shared_context_->insertRecording(info));
}
}
diff --git a/tools/metrics/histograms/metadata/gpu/enums.xml b/tools/metrics/histograms/metadata/gpu/enums.xml
index 58471a1d69c65cdf559f98ec2e11550bc16520d2..b62d8b78d34bfec491bcd5e6c65ff63eaedf00dd 100644
--- a/tools/metrics/histograms/metadata/gpu/enums.xml
+++ b/tools/metrics/histograms/metadata/gpu/enums.xml
@@ -1104,6 +1104,19 @@ Called by update_gpu_driver_bug_workaround_entries.py.-->
<int value="10" label="NoKillForGpuProgressInCrashDump"/>
</enum>
+<!-- LINT.IfChange(GraphiteInsertRecordingStatus) -->
+
+<enum name="GraphiteInsertRecordingStatus">
+ <int value="0" label="Success"/>
+ <int value="1" label="InvalidRecording"/>
+ <int value="2" label="PromiseImageInstantiationFailed"/>
+ <int value="3" label="AddCommandsFailed"/>
+ <int value="4" label="AsyncShaderCompilesFailed"/>
+ <int value="5" label="OutOfOrderRecording"/>
+</enum>
+
+<!-- LINT.ThenChange(//gpu/command_buffer/service/graphite_shared_context.cc:InsertRecordingStatusUma) -->
+
<enum name="HasDiscreteGpu">
<int value="0" label="No"/>
<int value="1" label="Yes"/>
diff --git a/tools/metrics/histograms/metadata/gpu/histograms.xml b/tools/metrics/histograms/metadata/gpu/histograms.xml
index ea8b129a2f4aba4d8afd9c3338f21574a12185dd..80bf969cbf66eac7f970b7c096da83c69424c38a 100644
--- a/tools/metrics/histograms/metadata/gpu/histograms.xml
+++ b/tools/metrics/histograms/metadata/gpu/histograms.xml
@@ -1004,6 +1004,16 @@ chromium-metrics-reviews@google.com.
</summary>
</histogram>
+<histogram name="Gpu.Graphite.InsertRecordingStatus"
+ enum="GraphiteInsertRecordingStatus" expires_after="2026-04-30">
+ <owner>sunnyps@chromium.org</owner>
+ <owner>michaelludwig@google.com</owner>
+ <owner>chrome-gpu-metric-alerts@chromium.org</owner>
+ <summary>
+ The return value for each Graphite insertRecording call made by Chromium.
+ </summary>
+</histogram>
+
<histogram name="GPU.GraphValidation.Duration" units="ms"
expires_after="2025-11-15">
<owner>vasilyt@chromium.org</owner>

View File

@@ -81,7 +81,7 @@ index 4bf9a3c27e05c6635b2beb8e880b5b43dbed61b5..f328fbb49c45991f44a9c75325491d08
} // namespace base
diff --git a/base/process/launch_mac.cc b/base/process/launch_mac.cc
index b63d58da9837ba4d1e4aff8f24f2cd977c5ed02d..8387fd7d2bcf8951b6cc024829c16d970799190c 100644
index b63d58da9837ba4d1e4aff8f24f2cd977c5ed02d..49b4c0b69731386ef5a4b7dfb782aa8f4ae09cdd 100644
--- a/base/process/launch_mac.cc
+++ b/base/process/launch_mac.cc
@@ -84,6 +84,10 @@ int posix_spawnattr_set_csm_np(const posix_spawnattr_t*, uint32_t)
@@ -135,15 +135,26 @@ index b63d58da9837ba4d1e4aff8f24f2cd977c5ed02d..8387fd7d2bcf8951b6cc024829c16d97
}
#endif
@@ -301,7 +321,7 @@ Process LaunchProcess(const std::vector<std::string>& argv,
@@ -301,16 +321,16 @@ Process LaunchProcess(const std::vector<std::string>& argv,
file_actions.Inherit(STDERR_FILENO);
}
-#if BUILDFLAG(IS_MAC)
+#if 0
+#if !IS_MAS_BUILD()
if (options.disclaim_responsibility) {
DPSXCHECK(responsibility_spawnattrs_setdisclaim(attr.get(), 1));
}
+#endif
EnvironmentMap new_environment_map = options.environment;
+#if !IS_MAS_BUILD()
MachPortRendezvousServerMac::AddFeatureStateToEnvironment(
new_environment_map);
-#else
- const EnvironmentMap& new_environment_map = options.environment;
#endif
std::vector<char*> argv_cstr;
diff --git a/base/process/process_info_mac.mm b/base/process/process_info_mac.mm
index e12c1d078147d956a1d9b1bc498c1b1d6fe7b974..233362259dc4e728ed37435e650417647b45a6af 100644
--- a/base/process/process_info_mac.mm

View File

@@ -0,0 +1,114 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: AbdAlRahman Gad <agad@igalia.com>
Date: Wed, 15 Oct 2025 09:34:12 -0700
Subject: Ozone/Wayland: Treat DND drop performed with NONE action as a
cancellation
According to the Wayland protocol, a "drop performed" event can be
followed with a `cancelled` event. This is the behavior of compositors
like KWin.
We were always treating "drop performed" as a completed drop. This
change corrects the logic to pass `DragResult::kCancelled` in this
scenario.
Bug: 447037092
Change-Id: I0f3805365355bb364e15a9ab6d5a6954698cce1f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7002773
Reviewed-by: Nick Yamane <nickdiego@igalia.com>
Reviewed-by: Max Ihlenfeldt <max@igalia.com>
Commit-Queue: AbdAlRahman Gad <agad@igalia.com>
Cr-Commit-Position: refs/heads/main@{#1530269}
diff --git a/ui/ozone/platform/wayland/host/wayland_data_device.h b/ui/ozone/platform/wayland/host/wayland_data_device.h
index 128baea3fe9a03f9d76afa234d6c21e9a4583cf7..d9824490e402308e64b5f54cbf9b46646f6021e8 100644
--- a/ui/ozone/platform/wayland/host/wayland_data_device.h
+++ b/ui/ozone/platform/wayland/host/wayland_data_device.h
@@ -90,6 +90,8 @@ class WaylandDataDevice : public WaylandDataDeviceBase {
FRIEND_TEST_ALL_PREFIXES(WaylandDataDragControllerTest, StartDrag);
FRIEND_TEST_ALL_PREFIXES(WaylandDataDragControllerTest, ReceiveDrag);
FRIEND_TEST_ALL_PREFIXES(WaylandDataDragControllerTest, CancelIncomingDrag);
+ FRIEND_TEST_ALL_PREFIXES(WaylandDataDragControllerTest,
+ DndDropPerformedWithNoneActionThenCancelled);
FRIEND_TEST_ALL_PREFIXES(WaylandDataDragControllerTest,
DestroyWindowWhileFetchingForeignData);
FRIEND_TEST_ALL_PREFIXES(WaylandDataDragControllerTest,
diff --git a/ui/ozone/platform/wayland/host/wayland_data_drag_controller.cc b/ui/ozone/platform/wayland/host/wayland_data_drag_controller.cc
index 893bf03a8f4aa473faf3a1232ea1902d226d8ca6..b557e4654dbe2a5d6f94bf31466c78bf83744c32 100644
--- a/ui/ozone/platform/wayland/host/wayland_data_drag_controller.cc
+++ b/ui/ozone/platform/wayland/host/wayland_data_drag_controller.cc
@@ -573,7 +573,13 @@ void WaylandDataDragController::OnDataSourceDropPerformed(
<< " origin=" << !!origin_window_
<< " nested_dispatcher=" << !!nested_dispatcher_;
- HandleDragEnd(DragResult::kCompleted, timestamp);
+ // Treat a "drop performed" event with a `dnd_action` of NONE (0) as a
+ // cancellation (passing `kCancelled`). Per the protocol, `cancelled` event
+ // can be sent after "drop performed", that is what `KWin` does, for example.
+ // See crbug.com/447037092.
+ HandleDragEnd(data_source_->dnd_action() ? DragResult::kCompleted
+ : DragResult::kCancelled,
+ timestamp);
}
void WaylandDataDragController::OnDataSourceSend(WaylandDataSource* source,
diff --git a/ui/ozone/platform/wayland/host/wayland_data_drag_controller_unittest.cc b/ui/ozone/platform/wayland/host/wayland_data_drag_controller_unittest.cc
index c70a03d082f518b48dda58be893fc9181507e1ab..baf42205a786d42a341a6dc9265c8d929c6a7454 100644
--- a/ui/ozone/platform/wayland/host/wayland_data_drag_controller_unittest.cc
+++ b/ui/ozone/platform/wayland/host/wayland_data_drag_controller_unittest.cc
@@ -497,6 +497,36 @@ MATCHER_P(PointFNear, n, "") {
return arg.IsWithinDistance(n, 0.01f);
}
+// Tests that if the compositor sends wl_data_source.dnd_drop_performed with
+// DND_ACTION_NONE, the drag controller treats it as a cancelled operation by
+// calling OnDragLeave, and can still handle a subsequent
+// wl_data_source.cancelled event gracefully. Regression test for
+// https://crbug.com/447037092.
+TEST_P(WaylandDataDragControllerTest,
+ DndDropPerformedWithNoneActionThenCancelled) {
+ FocusAndPressLeftPointerButton(window_.get(), &delegate_);
+
+ // Post test task to be performed asynchronously once the dnd-related protocol
+ // objects are ready.
+ ScheduleTestTask(base::BindLambdaForTesting([&]() {
+ // Now the server can read the data and give it to our callback.
+ ReadAndCheckData(kMimeTypeUtf8PlainText, kSampleTextForDragAndDrop);
+
+ EXPECT_CALL(*drop_handler_, OnDragLeave()).Times(1);
+ SendDndDropPerformed();
+
+ // Emulate server sending an wl_data_source::cancelled event so the drag
+ // loop is finished.
+ EXPECT_CALL(*drop_handler_, OnDragLeave()).Times(0);
+ SendDndCancelled();
+ }));
+
+ RunMouseDragWithSampleData(window_.get(), DragDropTypes::DRAG_NONE);
+
+ // Ensure drag delegate it properly reset when the drag loop quits.
+ EXPECT_FALSE(data_device()->drag_delegate_);
+}
+
TEST_P(WaylandDataDragControllerTest, ReceiveDrag) {
const uint32_t surface_id = window_->root_surface()->get_surface_id();
diff --git a/ui/ozone/platform/wayland/host/wayland_data_source.cc b/ui/ozone/platform/wayland/host/wayland_data_source.cc
index 761f6a456456e11ad0e8c4dbe408c360de50f02d..f1b74febf52ecadef0da6574d50dfe3709ee48d7 100644
--- a/ui/ozone/platform/wayland/host/wayland_data_source.cc
+++ b/ui/ozone/platform/wayland/host/wayland_data_source.cc
@@ -30,12 +30,12 @@ DataSource<T>::DataSource(T* data_source,
DCHECK(delegate_);
Initialize();
- VLOG(1) << "DataSoure created:" << this;
+ VLOG(1) << "DataSource created:" << this;
}
template <typename T>
DataSource<T>::~DataSource() {
- VLOG(1) << "DataSoure deleted:" << this;
+ VLOG(1) << "DataSource deleted:" << this;
}
template <typename T>

View File

@@ -12,5 +12,6 @@
{ "patch_dir": "src/electron/patches/ReactiveObjC", "repo": "src/third_party/squirrel.mac/vendor/ReactiveObjC" },
{ "patch_dir": "src/electron/patches/webrtc", "repo": "src/third_party/webrtc" },
{ "patch_dir": "src/electron/patches/reclient-configs", "repo": "src/third_party/engflow-reclient-configs" },
{ "patch_dir": "src/electron/patches/sqlite", "repo": "src/third_party/sqlite/src" }
{ "patch_dir": "src/electron/patches/sqlite", "repo": "src/third_party/sqlite/src" },
{ "patch_dir": "src/electron/patches/skia", "repo": "src/third_party/skia" }
]

1
patches/skia/.patches Normal file
View File

@@ -0,0 +1 @@
graphite_add_insertstatus_koutoforderrecording.patch

View File

@@ -0,0 +1,54 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Michael Ludwig <michaelludwig@google.com>
Date: Fri, 16 Jan 2026 15:28:27 -0500
Subject: [graphite] Add InsertStatus::kOutOfOrderRecording
Bug: b/458722690
Change-Id: I1c3661b9c765f46f039f0044ae3557a0e7f619cf
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1143598
Auto-Submit: Michael Ludwig <michaelludwig@google.com>
Reviewed-by: Thomas Smith <thomsmit@google.com>
Commit-Queue: Nicolette Prevost <nicolettep@google.com>
Reviewed-by: Nicolette Prevost <nicolettep@google.com>
diff --git a/include/gpu/graphite/GraphiteTypes.h b/include/gpu/graphite/GraphiteTypes.h
index a638da8988b2e8963255537c36f98d0233c917e8..a8e40871531ce0c2a2b98a4c8ba1c44e9b374c2d 100644
--- a/include/gpu/graphite/GraphiteTypes.h
+++ b/include/gpu/graphite/GraphiteTypes.h
@@ -52,7 +52,12 @@ public:
kAddCommandsFailed,
// Internal failure, shader pipeline compilation failed (driver issue, or disk corruption),
// state unrecoverable.
- kAsyncShaderCompilesFailed
+ kAsyncShaderCompilesFailed,
+ // The inserted Recording is out of order from what the Context expects (when
+ // `[Context|Recorder]Options::fRequireOrderedRecordings` is true), which can either
+ // represent a client synchronization error or an internal failure when a prior dependent
+ // Recording failed for some reason, no CB changes but state likely unrecoverable.
+ kOutOfOrderRecording,
};
constexpr InsertStatus() : fValue(kSuccess) {}
diff --git a/relnotes/out_of_order_status.md b/relnotes/out_of_order_status.md
new file mode 100644
index 0000000000000000000000000000000000000000..74f7c889338925d132a2d80cdba40d321807c614
--- /dev/null
+++ b/relnotes/out_of_order_status.md
@@ -0,0 +1,4 @@
+* Graphite's InsertStatus now has an additional kOutOfOrderRecording to differentiate this
+ unrecoverable error from programming errors that would lead to kInvalidRecording. Out of order
+ recordings can currently arise "naturally" if prior dependent recordings failed due to resource
+ creation or update errors from the GPU driver.
diff --git a/src/gpu/graphite/QueueManager.cpp b/src/gpu/graphite/QueueManager.cpp
index 2948e071390095f3a1a1d7fdb6c7878adeaf9c53..f5f97f23e4957b819f9d92e8f03f49cd6fad1ab3 100644
--- a/src/gpu/graphite/QueueManager.cpp
+++ b/src/gpu/graphite/QueueManager.cpp
@@ -122,7 +122,7 @@ InsertStatus QueueManager::addRecording(const InsertRecordingInfo& info, Context
if (recorderID != SK_InvalidGenID) {
uint32_t* recordingID = fLastAddedRecordingIDs.find(recorderID);
RETURN_FAIL_IF(recordingID && info.fRecording->priv().uniqueID() != *recordingID + 1,
- InsertStatus::kInvalidRecording,
+ InsertStatus::kOutOfOrderRecording,
"Recordings are expected to be replayed in order");
// Note the new Recording ID.

View File

@@ -9,3 +9,4 @@ refactor_use_non-deprecated_nskeyedarchiver_apis.patch
chore_turn_off_launchapplicationaturl_deprecation_errors_in_squirrel.patch
fix_crash_when_process_to_extract_zip_cannot_be_launched.patch
use_uttype_class_instead_of_deprecated_uttypeconformsto.patch
fix_clean_up_old_staged_updates_before_downloading_new_update.patch

View File

@@ -0,0 +1,64 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Andy Locascio <loc@anthropic.com>
Date: Tue, 6 Jan 2026 08:23:03 -0800
Subject: fix: clean up old staged updates before downloading new update
When checkForUpdates() is called while an update is already staged,
Squirrel creates a new temporary directory for the download without
cleaning up the old one. This can lead to significant disk usage if
the app keeps checking for updates without restarting.
This change adds a force parameter to pruneUpdateDirectories that
bypasses the AwaitingRelaunch state check. This is called before
creating a new temp directory, ensuring old staged updates are
cleaned up when a new download starts.
diff --git a/Squirrel/SQRLUpdater.m b/Squirrel/SQRLUpdater.m
index d156616e81e6f25a3bded30e6216b8fc311f31bc..6cd4346bf43b191147aff819cb93387e71275a46 100644
--- a/Squirrel/SQRLUpdater.m
+++ b/Squirrel/SQRLUpdater.m
@@ -543,11 +543,17 @@ - (RACSignal *)downloadBundleForUpdate:(SQRLUpdate *)update intoDirectory:(NSURL
#pragma mark File Management
- (RACSignal *)uniqueTemporaryDirectoryForUpdate {
- return [[[RACSignal
+ // Clean up any old staged update directories before creating a new one.
+ // This prevents disk usage from growing when checkForUpdates() is called
+ // multiple times without the app restarting.
+ return [[[[[self
+ pruneUpdateDirectoriesWithForce:YES]
+ ignoreValues]
+ concat:[RACSignal
defer:^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
return [directoryManager storageURL];
- }]
+ }]]
flattenMap:^(NSURL *storageURL) {
NSURL *updateDirectoryTemplate = [storageURL URLByAppendingPathComponent:[SQRLUpdaterUniqueTemporaryDirectoryPrefix stringByAppendingString:@"XXXXXXX"]];
char *updateDirectoryCString = strdup(updateDirectoryTemplate.path.fileSystemRepresentation);
@@ -643,7 +649,7 @@ - (BOOL)isRunningOnReadOnlyVolume {
- (RACSignal *)performHousekeeping {
return [[RACSignal
- merge:@[ [self pruneUpdateDirectories], [self truncateLogs] ]]
+ merge:@[ [self pruneUpdateDirectoriesWithForce:NO], [self truncateLogs] ]]
catch:^(NSError *error) {
NSLog(@"Error doing housekeeping: %@", error);
return [RACSignal empty];
@@ -658,11 +664,12 @@ - (RACSignal *)performHousekeeping {
///
/// Sends each removed directory then completes, or errors, on an unspecified
/// thread.
-- (RACSignal *)pruneUpdateDirectories {
+- (RACSignal *)pruneUpdateDirectoriesWithForce:(BOOL)force {
return [[[RACSignal
defer:^{
- // If we already have updates downloaded we don't wanna prune them.
- if (self.state == SQRLUpdaterStateAwaitingRelaunch) return [RACSignal empty];
+ // If we already have updates downloaded we don't wanna prune them,
+ // unless force is YES (used when starting a new download).
+ if (!force && self.state == SQRLUpdaterStateAwaitingRelaunch) return [RACSignal empty];
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
return [directoryManager storageURL];

View File

@@ -0,0 +1,32 @@
const yaml = require('yaml');
const fs = require('node:fs');
const path = require('node:path');
const PREFIX = '# AUTOGENERATED FILE - DO NOT EDIT MANUALLY\n# ONLY EDIT .github/workflows/pipeline-segment-electron-build.yml\n\n';
const base = path.resolve(__dirname, '../.github/workflows/pipeline-segment-electron-build.yml');
const target = path.resolve(__dirname, '../.github/workflows/pipeline-segment-electron-publish.yml');
const baseContents = fs.readFileSync(base, 'utf-8');
const parsedBase = yaml.parse(baseContents);
parsedBase.jobs.build.permissions = {
'artifact-metadata': 'write',
attestations: 'write',
contents: 'read',
'id-token': 'write'
};
if (process.argv.includes('--check')) {
if (fs.readFileSync(target, 'utf-8') !== PREFIX + yaml.stringify(parsedBase)) {
console.error(`${target} is out of date`);
console.error('Please run "copy-pipeline-segment-publish.js" to update it');
process.exit(1);
}
} else {
fs.writeFileSync(
target,
PREFIX + yaml.stringify(parsedBase)
);
}

View File

@@ -368,6 +368,14 @@ def upload_io_to_github(release, filename, filepath, version):
sys.stdout.buffer.write(c)
sys.stdout.flush()
if "GITHUB_OUTPUT" in os.environ:
output_path = os.environ["GITHUB_OUTPUT"]
with open(output_path, "r+", encoding='utf-8') as github_output:
if len(github_output.readlines()) > 0:
github_output.write(",")
else:
github_output.write('UPLOADED_PATHS=')
github_output.write(filepath)
def upload_sha256_checksum(version, file_path, key_prefix=None):
checksum_path = f'{file_path}.sha256sum'

View File

@@ -11,14 +11,9 @@
#include "base/logging.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "shell/browser/browser.h"
#include "shell/browser/javascript_environment.h"
#include "shell/browser/native_window.h"
#include "shell/browser/window_list.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/error_thrower.h"
#include "shell/common/gin_helper/promise.h"
@@ -33,16 +28,10 @@
#include <windows.foundation.metadata.h>
#include <windows.h>
#include <windows.management.deployment.h>
// Use pre-generated C++/WinRT headers from //third_party/nearby instead of the
// SDK's cppwinrt headers, which are missing implementation files.
#include "third_party/nearby/src/internal/platform/implementation/windows/generated/winrt/Windows.ApplicationModel.h"
#include "third_party/nearby/src/internal/platform/implementation/windows/generated/winrt/Windows.Foundation.Collections.h"
#include "third_party/nearby/src/internal/platform/implementation/windows/generated/winrt/Windows.Foundation.Metadata.h"
#include "third_party/nearby/src/internal/platform/implementation/windows/generated/winrt/Windows.Foundation.h"
#include "third_party/nearby/src/internal/platform/implementation/windows/generated/winrt/Windows.Management.Deployment.h"
#include "third_party/nearby/src/internal/platform/implementation/windows/generated/winrt/base.h"
#include <wrl.h>
#include "base/win/scoped_com_initializer.h"
#include "base/win/core_winrt_util.h"
#include "base/win/scoped_hstring.h"
#endif
namespace electron {
@@ -55,6 +44,53 @@ const bool debug_msix_updater =
namespace {
#if BUILDFLAG(IS_WIN)
// Type aliases for cleaner code
using ABI::Windows::ApplicationModel::IAppInstallerInfo;
using ABI::Windows::ApplicationModel::IPackage;
using ABI::Windows::ApplicationModel::IPackage2;
using ABI::Windows::ApplicationModel::IPackage4;
using ABI::Windows::ApplicationModel::IPackage6;
using ABI::Windows::ApplicationModel::IPackageId;
using ABI::Windows::ApplicationModel::IPackageStatics;
using ABI::Windows::ApplicationModel::PackageSignatureKind;
using ABI::Windows::ApplicationModel::PackageSignatureKind_Developer;
using ABI::Windows::ApplicationModel::PackageSignatureKind_Enterprise;
using ABI::Windows::ApplicationModel::PackageSignatureKind_None;
using ABI::Windows::ApplicationModel::PackageSignatureKind_Store;
using ABI::Windows::ApplicationModel::PackageSignatureKind_System;
using ABI::Windows::Foundation::AsyncStatus;
using ABI::Windows::Foundation::IAsyncInfo;
using ABI::Windows::Foundation::IUriRuntimeClass;
using ABI::Windows::Foundation::IUriRuntimeClassFactory;
using ABI::Windows::Foundation::Metadata::IApiInformationStatics;
using ABI::Windows::Management::Deployment::DeploymentOptions;
using ABI::Windows::Management::Deployment::
DeploymentOptions_ForceApplicationShutdown;
using ABI::Windows::Management::Deployment::
DeploymentOptions_ForceTargetApplicationShutdown;
using ABI::Windows::Management::Deployment::
DeploymentOptions_ForceUpdateFromAnyVersion;
using ABI::Windows::Management::Deployment::DeploymentOptions_None;
using ABI::Windows::Management::Deployment::IAddPackageOptions;
using ABI::Windows::Management::Deployment::IDeploymentResult;
using ABI::Windows::Management::Deployment::IPackageManager;
using ABI::Windows::Management::Deployment::IPackageManager5;
using ABI::Windows::Management::Deployment::IPackageManager9;
using Microsoft::WRL::Callback;
using Microsoft::WRL::ComPtr;
// Type alias for deployment async operation
// AddPackageByUriAsync returns IAsyncOperationWithProgress<DeploymentResult*,
// DeploymentProgress>
using DeploymentAsyncOp = ABI::Windows::Foundation::IAsyncOperationWithProgress<
ABI::Windows::Management::Deployment::DeploymentResult*,
ABI::Windows::Management::Deployment::DeploymentProgress>;
using DeploymentCompletedHandler =
ABI::Windows::Foundation::IAsyncOperationWithProgressCompletedHandler<
ABI::Windows::Management::Deployment::DeploymentResult*,
ABI::Windows::Management::Deployment::DeploymentProgress>;
// Helper function for debug logging
void DebugLog(std::string_view log_msg) {
if (electron::debug_msix_updater)
@@ -84,32 +120,274 @@ struct RegisterPackageOptions {
bool force_update_from_any_version = false;
};
// Helper: Create PackageManager using RoActivateInstance
//
// Note on COM interface versioning: In COM/WinRT, each interface version
// (IPackageManager, IPackageManager5, IPackageManager9, etc.) is a separate
// interface that must be queried independently. Unlike C++ inheritance,
// IPackageManager9 does NOT inherit methods from IPackageManager5 or the base
// IPackageManager. Each version only contains the methods that were newly
// added in that version. To call methods from different versions, you must
// QueryInterface (or ComPtr::As) for each specific interface version needed.
HRESULT CreatePackageManager(ComPtr<IPackageManager>* package_manager) {
base::win::ScopedHString class_id = base::win::ScopedHString::Create(
RuntimeClass_Windows_Management_Deployment_PackageManager);
if (!class_id.is_valid()) {
return E_FAIL;
}
ComPtr<IInspectable> inspectable;
HRESULT hr = base::win::RoActivateInstance(class_id.get(), &inspectable);
if (FAILED(hr)) {
return hr;
}
return inspectable.As(package_manager);
}
// Helper: Create URI using IUriRuntimeClassFactory
HRESULT CreateUri(const std::wstring& uri_string,
ComPtr<IUriRuntimeClass>* uri) {
ComPtr<IUriRuntimeClassFactory> uri_factory;
HRESULT hr =
base::win::GetActivationFactory<IUriRuntimeClassFactory,
RuntimeClass_Windows_Foundation_Uri>(
&uri_factory);
if (FAILED(hr)) {
return hr;
}
base::win::ScopedHString uri_hstring =
base::win::ScopedHString::Create(uri_string);
if (!uri_hstring.is_valid()) {
return E_FAIL;
}
return uri_factory->CreateUri(uri_hstring.get(), uri->GetAddressOf());
}
// Helper: Create and configure AddPackageOptions
HRESULT CreateAddPackageOptions(const UpdateMsixOptions& opts,
ComPtr<IAddPackageOptions>* package_options) {
base::win::ScopedHString class_id = base::win::ScopedHString::Create(
RuntimeClass_Windows_Management_Deployment_AddPackageOptions);
if (!class_id.is_valid()) {
return E_FAIL;
}
ComPtr<IInspectable> inspectable;
HRESULT hr = base::win::RoActivateInstance(class_id.get(), &inspectable);
if (FAILED(hr)) {
return hr;
}
hr = inspectable.As(package_options);
if (FAILED(hr)) {
return hr;
}
// Configure options using ABI interface methods
(*package_options)
->put_DeferRegistrationWhenPackagesAreInUse(opts.defer_registration);
(*package_options)->put_DeveloperMode(opts.developer_mode);
(*package_options)->put_ForceAppShutdown(opts.force_shutdown);
(*package_options)->put_ForceTargetAppShutdown(opts.force_target_shutdown);
(*package_options)
->put_ForceUpdateFromAnyVersion(opts.force_update_from_any_version);
return S_OK;
}
// Helper: Check if API contract is present
HRESULT CheckApiContractPresent(UINT16 version, boolean* is_present) {
ComPtr<IApiInformationStatics> api_info;
HRESULT hr = base::win::GetActivationFactory<
IApiInformationStatics,
RuntimeClass_Windows_Foundation_Metadata_ApiInformation>(&api_info);
if (FAILED(hr)) {
return hr;
}
base::win::ScopedHString contract_name = base::win::ScopedHString::Create(
L"Windows.Foundation.UniversalApiContract");
if (!contract_name.is_valid()) {
return E_FAIL;
}
return api_info->IsApiContractPresentByMajor(contract_name.get(), version,
is_present);
}
// Helper: Get current package using IPackageStatics
HRESULT GetCurrentPackage(ComPtr<IPackage>* package) {
ComPtr<IPackageStatics> package_statics;
HRESULT hr = base::win::GetActivationFactory<
IPackageStatics, RuntimeClass_Windows_ApplicationModel_Package>(
&package_statics);
if (FAILED(hr)) {
return hr;
}
return package_statics->get_Current(package->GetAddressOf());
}
// Structure to hold callback data for async operations
struct DeploymentCallbackData {
scoped_refptr<base::SingleThreadTaskRunner> reply_runner;
gin_helper::Promise<void> promise;
bool fire_and_forget;
ComPtr<DeploymentAsyncOp> async_op; // Keep async_op alive
std::string operation_name; // "Deployment" or "Registration" for logs
};
// Handler for deployment/registration completion
void OnDeploymentCompleted(std::unique_ptr<DeploymentCallbackData> data,
DeploymentAsyncOp* async_op,
AsyncStatus status) {
std::string error;
const std::string& op_name = data->operation_name;
if (data->fire_and_forget) {
std::ostringstream oss;
oss << op_name
<< " initiated. Force shutdown or target shutdown requested. "
"Good bye!";
DebugLog(oss.str());
// Don't wait for result in fire-and-forget mode
data->reply_runner->PostTask(
FROM_HERE,
base::BindOnce(
[](gin_helper::Promise<void> promise) { promise.Resolve(); },
std::move(data->promise)));
return;
}
if (status == AsyncStatus::Error) {
ComPtr<IDeploymentResult> result;
HRESULT hr = async_op->GetResults(&result);
if (SUCCEEDED(hr) && result) {
HSTRING error_text_hstring;
hr = result->get_ErrorText(&error_text_hstring);
if (SUCCEEDED(hr)) {
base::win::ScopedHString scoped_error(error_text_hstring);
error = scoped_error.GetAsUTF8();
}
ComPtr<IAsyncInfo> async_info;
hr = async_op->QueryInterface(IID_PPV_ARGS(&async_info));
if (SUCCEEDED(hr)) {
HRESULT error_code;
hr = async_info->get_ErrorCode(&error_code);
if (SUCCEEDED(hr)) {
error += " (" + std::to_string(static_cast<int>(error_code)) + ")";
}
}
}
if (error.empty()) {
error = op_name + " failed with unknown error";
}
{
std::ostringstream oss;
oss << op_name << " failed: " << error;
DebugLog(oss.str());
}
} else if (status == AsyncStatus::Canceled) {
std::ostringstream oss;
oss << op_name << " canceled";
DebugLog(oss.str());
error = op_name + " canceled";
} else if (status == AsyncStatus::Completed) {
std::ostringstream oss;
oss << "MSIX " << op_name << " completed.";
DebugLog(oss.str());
} else {
error = op_name + " status unknown";
std::ostringstream oss;
oss << op_name << " status unknown";
DebugLog(oss.str());
}
// Post result back to UI thread
data->reply_runner->PostTask(
FROM_HERE, base::BindOnce(
[](gin_helper::Promise<void> promise, std::string error) {
if (error.empty()) {
promise.Resolve();
} else {
promise.RejectWithErrorMessage(error);
}
},
std::move(data->promise), std::move(error)));
}
// Performs MSIX update on IO thread
void DoUpdateMsix(const std::string& package_uri,
UpdateMsixOptions opts,
scoped_refptr<base::SingleThreadTaskRunner> reply_runner,
gin_helper::Promise<void> promise) {
DebugLog("DoUpdateMsix: Starting");
using winrt::Windows::Foundation::AsyncStatus;
using winrt::Windows::Foundation::Uri;
using winrt::Windows::Management::Deployment::AddPackageOptions;
using winrt::Windows::Management::Deployment::DeploymentResult;
using winrt::Windows::Management::Deployment::PackageManager;
std::string error;
std::wstring packageUriString =
std::wstring(package_uri.begin(), package_uri.end());
Uri uri{packageUriString};
PackageManager packageManager;
AddPackageOptions packageOptions;
// Use the pre-parsed options
packageOptions.DeferRegistrationWhenPackagesAreInUse(opts.defer_registration);
packageOptions.DeveloperMode(opts.developer_mode);
packageOptions.ForceAppShutdown(opts.force_shutdown);
packageOptions.ForceTargetAppShutdown(opts.force_target_shutdown);
packageOptions.ForceUpdateFromAnyVersion(opts.force_update_from_any_version);
// Create PackageManager
ComPtr<IPackageManager> package_manager;
HRESULT hr = CreatePackageManager(&package_manager);
if (FAILED(hr)) {
error = "Failed to create PackageManager";
reply_runner->PostTask(
FROM_HERE,
base::BindOnce(
[](gin_helper::Promise<void> promise, std::string error) {
promise.RejectWithErrorMessage(error);
},
std::move(promise), std::move(error)));
return;
}
// Get IPackageManager9 for AddPackageByUriAsync
ComPtr<IPackageManager9> package_manager9;
hr = package_manager.As(&package_manager9);
if (FAILED(hr)) {
error = "Failed to get IPackageManager9 interface";
reply_runner->PostTask(
FROM_HERE,
base::BindOnce(
[](gin_helper::Promise<void> promise, std::string error) {
promise.RejectWithErrorMessage(error);
},
std::move(promise), std::move(error)));
return;
}
// Create URI
std::wstring uri_wstring = base::UTF8ToWide(package_uri);
ComPtr<IUriRuntimeClass> uri;
hr = CreateUri(uri_wstring, &uri);
if (FAILED(hr)) {
error = "Failed to create URI";
reply_runner->PostTask(
FROM_HERE,
base::BindOnce(
[](gin_helper::Promise<void> promise, std::string error) {
promise.RejectWithErrorMessage(error);
},
std::move(promise), std::move(error)));
return;
}
// Create AddPackageOptions
ComPtr<IAddPackageOptions> package_options;
hr = CreateAddPackageOptions(opts, &package_options);
if (FAILED(hr)) {
error = "Failed to create AddPackageOptions";
reply_runner->PostTask(
FROM_HERE,
base::BindOnce(
[](gin_helper::Promise<void> promise, std::string error) {
promise.RejectWithErrorMessage(error);
},
std::move(promise), std::move(error)));
return;
}
{
std::ostringstream oss;
@@ -127,63 +405,54 @@ void DoUpdateMsix(const std::string& package_uri,
DebugLog(oss.str());
}
auto deploymentOperation =
packageManager.AddPackageByUriAsync(uri, packageOptions);
if (!deploymentOperation) {
DebugLog("Deployment operation is null");
// Start async operation
ComPtr<DeploymentAsyncOp> async_op;
hr = package_manager9->AddPackageByUriAsync(uri.Get(), package_options.Get(),
&async_op);
if (FAILED(hr) || !async_op) {
DebugLog("AddPackageByUriAsync failed or returned null");
error =
"Deployment is NULL. See "
"http://go.microsoft.com/fwlink/?LinkId=235160 for diagnosing.";
} else {
if (!opts.force_shutdown && !opts.force_target_shutdown) {
DebugLog("Waiting for deployment...");
deploymentOperation.get();
DebugLog("Deployment finished.");
if (deploymentOperation.Status() == AsyncStatus::Error) {
auto deploymentResult{deploymentOperation.GetResults()};
std::string errorText = winrt::to_string(deploymentResult.ErrorText());
std::string errorCode =
std::to_string(static_cast<int>(deploymentOperation.ErrorCode()));
error = errorText + " (" + errorCode + ")";
{
std::ostringstream oss;
oss << "Deployment failed: " << error;
DebugLog(oss.str());
}
} else if (deploymentOperation.Status() == AsyncStatus::Canceled) {
DebugLog("Deployment canceled");
error = "Deployment canceled";
} else if (deploymentOperation.Status() == AsyncStatus::Completed) {
DebugLog("MSIX Deployment completed.");
} else {
error = "Deployment status unknown";
DebugLog("Deployment status unknown");
}
} else {
// At this point, we can not await the deployment because we require a
// shutdown of the app to continue, so we do a fire and forget. When the
// deployment process tries ot shutdown the app, the process waits for us
// to finish here. But to finish we need to shutdow. That leads to a 30s
// dealock, till we forcefully get shutdown by the OS.
DebugLog(
"Deployment initiated. Force shutdown or target shutdown requested. "
"Good bye!");
}
reply_runner->PostTask(
FROM_HERE,
base::BindOnce(
[](gin_helper::Promise<void> promise, std::string error) {
promise.RejectWithErrorMessage(error);
},
std::move(promise), std::move(error)));
return;
}
// Post result back
reply_runner->PostTask(
FROM_HERE, base::BindOnce(
[](gin_helper::Promise<void> promise, std::string error) {
if (error.empty()) {
promise.Resolve();
} else {
promise.RejectWithErrorMessage(error);
}
},
std::move(promise), error));
// Set up callback data
auto callback_data = std::make_unique<DeploymentCallbackData>();
callback_data->reply_runner = reply_runner;
callback_data->promise = std::move(promise);
callback_data->fire_and_forget =
opts.force_shutdown || opts.force_target_shutdown;
callback_data->async_op = async_op; // Keep async_op alive
callback_data->operation_name = "Deployment";
// Register completion handler
DeploymentCallbackData* raw_data = callback_data.get();
hr = async_op->put_Completed(
Callback<DeploymentCompletedHandler>([data = std::move(callback_data)](
DeploymentAsyncOp* op,
AsyncStatus status) mutable {
OnDeploymentCompleted(std::move(data), op, status);
return S_OK;
}).Get());
if (FAILED(hr)) {
DebugLog("Failed to register completion handler");
raw_data->reply_runner->PostTask(
FROM_HERE, base::BindOnce(
[](gin_helper::Promise<void> promise) {
promise.RejectWithErrorMessage(
"Failed to register completion handler");
},
std::move(raw_data->promise)));
}
}
// Performs package registration on IO thread
@@ -192,31 +461,67 @@ void DoRegisterPackage(const std::string& family_name,
scoped_refptr<base::SingleThreadTaskRunner> reply_runner,
gin_helper::Promise<void> promise) {
DebugLog("DoRegisterPackage: Starting");
using winrt::Windows::Foundation::AsyncStatus;
using winrt::Windows::Foundation::Collections::IIterable;
using winrt::Windows::Management::Deployment::DeploymentOptions;
using winrt::Windows::Management::Deployment::PackageManager;
std::string error;
auto familyNameH = winrt::to_hstring(family_name);
PackageManager packageManager;
DeploymentOptions deploymentOptions = DeploymentOptions::None;
// Use the pre-parsed options (no V8 access needed)
// Create PackageManager
ComPtr<IPackageManager> package_manager;
HRESULT hr = CreatePackageManager(&package_manager);
if (FAILED(hr)) {
error = "Failed to create PackageManager";
reply_runner->PostTask(
FROM_HERE,
base::BindOnce(
[](gin_helper::Promise<void> promise, std::string error) {
promise.RejectWithErrorMessage(error);
},
std::move(promise), std::move(error)));
return;
}
// Get IPackageManager5 for RegisterPackageByFamilyNameAsync
ComPtr<IPackageManager5> package_manager5;
hr = package_manager.As(&package_manager5);
if (FAILED(hr)) {
error = "Failed to get IPackageManager5 interface";
reply_runner->PostTask(
FROM_HERE,
base::BindOnce(
[](gin_helper::Promise<void> promise, std::string error) {
promise.RejectWithErrorMessage(error);
},
std::move(promise), std::move(error)));
return;
}
// Build DeploymentOptions flags
DeploymentOptions deployment_options = DeploymentOptions_None;
if (opts.force_shutdown) {
deploymentOptions |= DeploymentOptions::ForceApplicationShutdown;
deployment_options = static_cast<DeploymentOptions>(
deployment_options | DeploymentOptions_ForceApplicationShutdown);
}
if (opts.force_target_shutdown) {
deploymentOptions |= DeploymentOptions::ForceTargetApplicationShutdown;
deployment_options = static_cast<DeploymentOptions>(
deployment_options | DeploymentOptions_ForceTargetApplicationShutdown);
}
if (opts.force_update_from_any_version) {
deploymentOptions |= DeploymentOptions::ForceUpdateFromAnyVersion;
deployment_options = static_cast<DeploymentOptions>(
deployment_options | DeploymentOptions_ForceUpdateFromAnyVersion);
}
// Create empty collections for dependency and optional packages
IIterable<winrt::hstring> emptyDependencies{nullptr};
IIterable<winrt::hstring> emptyOptional{nullptr};
// Create HSTRING for family name
base::win::ScopedHString family_name_hstring =
base::win::ScopedHString::Create(family_name);
if (!family_name_hstring.is_valid()) {
error = "Failed to create family name string";
reply_runner->PostTask(
FROM_HERE,
base::BindOnce(
[](gin_helper::Promise<void> promise, std::string error) {
promise.RejectWithErrorMessage(error);
},
std::move(promise), std::move(error)));
return;
}
{
std::ostringstream oss;
@@ -233,63 +538,59 @@ void DoRegisterPackage(const std::string& family_name,
DebugLog(oss.str());
}
auto deploymentOperation = packageManager.RegisterPackageByFamilyNameAsync(
familyNameH, emptyDependencies, deploymentOptions, nullptr,
emptyOptional);
// RegisterPackageByFamilyNameAndOptionalPackagesAsync (ABI name)
ComPtr<DeploymentAsyncOp> async_op;
hr = package_manager5->RegisterPackageByFamilyNameAndOptionalPackagesAsync(
family_name_hstring.get(),
nullptr, // dependencyPackageFamilyNames
deployment_options,
nullptr, // appDataVolume
nullptr, // optionalPackageFamilyNames
&async_op);
if (!deploymentOperation) {
if (FAILED(hr) || !async_op) {
error =
"Deployment is NULL. See "
"http://go.microsoft.com/fwlink/?LinkId=235160 for diagnosing.";
} else {
if (!opts.force_shutdown && !opts.force_target_shutdown) {
DebugLog("Waiting for registration...");
deploymentOperation.get();
DebugLog("Registration finished.");
if (deploymentOperation.Status() == AsyncStatus::Error) {
auto deploymentResult{deploymentOperation.GetResults()};
std::string errorText = winrt::to_string(deploymentResult.ErrorText());
std::string errorCode =
std::to_string(static_cast<int>(deploymentOperation.ErrorCode()));
error = errorText + " (" + errorCode + ")";
{
std::ostringstream oss;
oss << "Registration failed: " << error;
DebugLog(oss.str());
}
} else if (deploymentOperation.Status() == AsyncStatus::Canceled) {
DebugLog("Registration canceled");
error = "Registration canceled";
} else if (deploymentOperation.Status() == AsyncStatus::Completed) {
DebugLog("MSIX Registration completed.");
} else {
error = "Registration status unknown";
DebugLog("Registration status unknown");
}
} else {
// At this point, we can not await the registration because we require a
// shutdown of the app to continue, so we do a fire and forget. When the
// registration process tries ot shutdown the app, the process waits for
// us to finish here. But to finish we need to shutdown. That leads to a
// 30s dealock, till we forcefully get shutdown by the OS.
DebugLog(
"Registration initiated. Force shutdown or target shutdown "
"requested. Good bye!");
}
reply_runner->PostTask(
FROM_HERE,
base::BindOnce(
[](gin_helper::Promise<void> promise, std::string error) {
promise.RejectWithErrorMessage(error);
},
std::move(promise), std::move(error)));
return;
}
// Post result back to UI thread
reply_runner->PostTask(
FROM_HERE, base::BindOnce(
[](gin_helper::Promise<void> promise, std::string error) {
if (error.empty()) {
promise.Resolve();
} else {
promise.RejectWithErrorMessage(error);
}
},
std::move(promise), error));
// Set up callback data
auto callback_data = std::make_unique<DeploymentCallbackData>();
callback_data->reply_runner = reply_runner;
callback_data->promise = std::move(promise);
callback_data->fire_and_forget =
opts.force_shutdown || opts.force_target_shutdown;
callback_data->async_op = async_op; // Keep async_op alive
callback_data->operation_name = "Registration";
// Register completion handler
DeploymentCallbackData* raw_data = callback_data.get();
hr = async_op->put_Completed(
Callback<DeploymentCompletedHandler>([data = std::move(callback_data)](
DeploymentAsyncOp* op,
AsyncStatus status) mutable {
OnDeploymentCompleted(std::move(data), op, status);
return S_OK;
}).Get());
if (FAILED(hr)) {
DebugLog("Failed to register completion handler");
raw_data->reply_runner->PostTask(
FROM_HERE, base::BindOnce(
[](gin_helper::Promise<void> promise) {
promise.RejectWithErrorMessage(
"Failed to register completion handler");
},
std::move(raw_data->promise)));
}
}
#endif
@@ -307,6 +608,16 @@ v8::Local<v8::Promise> UpdateMsix(const std::string& package_uri,
return handle;
}
// Check for required API contract (IPackageManager9 requires v10)
boolean is_api_present = FALSE;
if (FAILED(CheckApiContractPresent(10, &is_api_present)) || !is_api_present) {
DebugLog("UpdateMsix: Required Windows API contract not present");
promise.RejectWithErrorMessage(
"This Windows version does not support MSIX updates via this API. "
"Windows 10 version 2004 or later is required.");
return handle;
}
// Parse options on UI thread (where V8 is available)
UpdateMsixOptions opts;
options.Get("deferRegistration", &opts.defer_registration);
@@ -349,6 +660,16 @@ v8::Local<v8::Promise> RegisterPackage(const std::string& family_name,
return handle;
}
// Check for required API contract (IPackageManager5 requires v3)
boolean is_api_present = FALSE;
if (FAILED(CheckApiContractPresent(3, &is_api_present)) || !is_api_present) {
DebugLog("RegisterPackage: Required Windows API contract not present");
promise.RejectWithErrorMessage(
"This Windows version does not support package registration via this "
"API. Windows 10 version 1607 or later is required.");
return handle;
}
// Parse options on UI thread (where V8 is available)
RegisterPackageOptions opts;
options.Get("forceShutdown", &opts.force_shutdown);
@@ -384,32 +705,30 @@ bool RegisterRestartOnUpdate(const std::string& command_line) {
return false;
}
const wchar_t* commandLine = nullptr;
// Flags: RESTART_NO_CRASH | RESTART_NO_HANG | RESTART_NO_REBOOT
// This means: only restart on updates (RESTART_NO_PATCH is NOT set)
const DWORD dwFlags = 1 | 2 | 8; // 11
// Convert command line to wide string (keep in scope for API call)
std::wstring command_line_wide;
const wchar_t* command_line_ptr = nullptr;
if (!command_line.empty()) {
std::wstring commandLineW =
std::wstring(command_line.begin(), command_line.end());
commandLine = commandLineW.c_str();
command_line_wide = base::UTF8ToWide(command_line);
command_line_ptr = command_line_wide.c_str();
}
HRESULT hr = RegisterApplicationRestart(commandLine, dwFlags);
HRESULT hr = RegisterApplicationRestart(command_line_ptr, dwFlags);
if (FAILED(hr)) {
{
std::ostringstream oss;
oss << "RegisterApplicationRestart failed with error code: " << hr;
DebugLog(oss.str());
}
std::ostringstream oss;
oss << "RegisterApplicationRestart failed with error code: " << hr;
DebugLog(oss.str());
return false;
}
{
std::ostringstream oss;
oss << "RegisterApplicationRestart succeeded"
<< (command_line.empty() ? "" : " with command line");
DebugLog(oss.str());
}
std::ostringstream oss;
oss << "RegisterApplicationRestart succeeded"
<< (command_line.empty() ? "" : " with command line");
DebugLog(oss.str());
return true;
#else
return false;
@@ -434,57 +753,119 @@ v8::Local<v8::Value> GetPackageInfo() {
gin_helper::Dictionary result(isolate, v8::Object::New(isolate));
// Check API contract version (Windows 10 version 1703 or later)
if (winrt::Windows::Foundation::Metadata::ApiInformation::
IsApiContractPresent(L"Windows.Foundation.UniversalApiContract", 7)) {
using winrt::Windows::ApplicationModel::Package;
using winrt::Windows::ApplicationModel::PackageSignatureKind;
Package package = Package::Current();
boolean is_present = FALSE;
HRESULT hr = CheckApiContractPresent(7, &is_present);
if (SUCCEEDED(hr) && is_present) {
ComPtr<IPackage> package;
hr = GetCurrentPackage(&package);
if (SUCCEEDED(hr) && package) {
// Query all needed package interface versions upfront.
// Note: Like IPackageManager, each IPackage version (IPackage2,
// IPackage4, IPackage6) is a separate COM interface. IPackage6 does NOT
// inherit methods from earlier versions. We must query each version
// separately to access its specific methods:
// - IPackage2: get_IsDevelopmentMode
// - IPackage4: get_SignatureKind
// - IPackage6: GetAppInstallerInfo
ComPtr<IPackage2> package2;
ComPtr<IPackage4> package4;
ComPtr<IPackage6> package6;
package.As(&package2);
package.As(&package4);
package.As(&package6);
// Get package ID and family name
std::string packageId = winrt::to_string(package.Id().FullName());
std::string familyName = winrt::to_string(package.Id().FamilyName());
// Get package ID (from base IPackage)
ComPtr<IPackageId> package_id;
hr = package->get_Id(&package_id);
if (SUCCEEDED(hr) && package_id) {
// Get FullName
HSTRING full_name;
hr = package_id->get_FullName(&full_name);
if (SUCCEEDED(hr)) {
base::win::ScopedHString scoped_name(full_name);
result.Set("id", scoped_name.GetAsUTF8());
}
result.Set("id", packageId);
result.Set("familyName", familyName);
result.Set("developmentMode", package.IsDevelopmentMode());
// Get FamilyName
HSTRING family_name;
hr = package_id->get_FamilyName(&family_name);
if (SUCCEEDED(hr)) {
base::win::ScopedHString scoped_name(family_name);
result.Set("familyName", scoped_name.GetAsUTF8());
}
// Get package version
auto packageVersion = package.Id().Version();
std::string version = std::to_string(packageVersion.Major) + "." +
std::to_string(packageVersion.Minor) + "." +
std::to_string(packageVersion.Build) + "." +
std::to_string(packageVersion.Revision);
result.Set("version", version);
// Get Version
ABI::Windows::ApplicationModel::PackageVersion pkg_version;
hr = package_id->get_Version(&pkg_version);
if (SUCCEEDED(hr)) {
std::string version = std::to_string(pkg_version.Major) + "." +
std::to_string(pkg_version.Minor) + "." +
std::to_string(pkg_version.Build) + "." +
std::to_string(pkg_version.Revision);
result.Set("version", version);
}
}
// Convert signature kind to string
std::string signatureKind;
switch (package.SignatureKind()) {
case PackageSignatureKind::Developer:
signatureKind = "developer";
break;
case PackageSignatureKind::Enterprise:
signatureKind = "enterprise";
break;
case PackageSignatureKind::None:
signatureKind = "none";
break;
case PackageSignatureKind::Store:
signatureKind = "store";
break;
case PackageSignatureKind::System:
signatureKind = "system";
break;
default:
signatureKind = "none";
break;
}
result.Set("signatureKind", signatureKind);
// Get IsDevelopmentMode (from IPackage2)
if (package2) {
boolean is_dev_mode = FALSE;
hr = package2->get_IsDevelopmentMode(&is_dev_mode);
result.Set("developmentMode", SUCCEEDED(hr) && is_dev_mode != FALSE);
} else {
result.Set("developmentMode", false);
}
// Get app installer info if available
auto appInstallerInfo = package.GetAppInstallerInfo();
if (appInstallerInfo != nullptr) {
std::string uriStr = winrt::to_string(appInstallerInfo.Uri().ToString());
result.Set("appInstallerUri", uriStr);
// Get SignatureKind (from IPackage4)
if (package4) {
PackageSignatureKind sig_kind;
hr = package4->get_SignatureKind(&sig_kind);
if (SUCCEEDED(hr)) {
std::string signature_kind;
switch (sig_kind) {
case PackageSignatureKind_Developer:
signature_kind = "developer";
break;
case PackageSignatureKind_Enterprise:
signature_kind = "enterprise";
break;
case PackageSignatureKind_None:
signature_kind = "none";
break;
case PackageSignatureKind_Store:
signature_kind = "store";
break;
case PackageSignatureKind_System:
signature_kind = "system";
break;
default:
signature_kind = "none";
break;
}
result.Set("signatureKind", signature_kind);
} else {
result.Set("signatureKind", "none");
}
} else {
result.Set("signatureKind", "none");
}
// Get AppInstallerInfo (from IPackage6)
if (package6) {
ComPtr<IAppInstallerInfo> app_installer_info;
hr = package6->GetAppInstallerInfo(&app_installer_info);
if (SUCCEEDED(hr) && app_installer_info) {
ComPtr<IUriRuntimeClass> uri;
hr = app_installer_info->get_Uri(&uri);
if (SUCCEEDED(hr) && uri) {
HSTRING uri_string;
hr = uri->get_AbsoluteUri(&uri_string);
if (SUCCEEDED(hr)) {
base::win::ScopedHString scoped_uri(uri_string);
result.Set("appInstallerUri", scoped_uri.GetAsUTF8());
}
}
}
}
}
} else {
// Windows version doesn't meet minimum API requirements

View File

@@ -71,7 +71,8 @@ UtilityProcessWrapper::UtilityProcessWrapper(
base::EnvironmentMap env_map,
base::FilePath current_working_directory,
bool use_plugin_helper,
bool create_network_observer) {
bool create_network_observer,
bool disclaim_responsibility) {
#if BUILDFLAG(IS_WIN)
base::win::ScopedHandle stdout_write(nullptr);
base::win::ScopedHandle stderr_write(nullptr);
@@ -184,6 +185,7 @@ UtilityProcessWrapper::UtilityProcessWrapper(
.WithChildFlags(use_plugin_helper
? content::ChildProcessHost::CHILD_PLUGIN
: content::ChildProcessHost::CHILD_NORMAL)
.WithDisclaimResponsibility(disclaim_responsibility)
#endif
.WithProcessCallback(
base::BindOnce(&UtilityProcessWrapper::OnServiceProcessLaunch,
@@ -453,6 +455,7 @@ gin_helper::Handle<UtilityProcessWrapper> UtilityProcessWrapper::Create(
std::u16string display_name;
bool use_plugin_helper = false;
bool create_network_observer = false;
bool disclaim_responsibility = false;
std::map<IOHandle, IOType> stdio;
base::FilePath current_working_directory;
base::EnvironmentMap env_map;
@@ -496,13 +499,15 @@ gin_helper::Handle<UtilityProcessWrapper> UtilityProcessWrapper::Create(
#if BUILDFLAG(IS_MAC)
opts.Get("allowLoadingUnsignedLibraries", &use_plugin_helper);
opts.Get("disclaim", &disclaim_responsibility);
#endif
}
auto handle = gin_helper::CreateHandle(
args->isolate(), new UtilityProcessWrapper(
std::move(params), display_name, std::move(stdio),
env_map, current_working_directory,
use_plugin_helper, create_network_observer));
args->isolate(),
new UtilityProcessWrapper(
std::move(params), display_name, std::move(stdio), env_map,
current_working_directory, use_plugin_helper, create_network_observer,
disclaim_responsibility));
handle->Pin(args->isolate());
return handle;
}

View File

@@ -72,7 +72,8 @@ class UtilityProcessWrapper final
base::EnvironmentMap env_map,
base::FilePath current_working_directory,
bool use_plugin_helper,
bool create_network_observer);
bool create_network_observer,
bool disclaim_responsibility);
void OnServiceProcessLaunch(const base::Process& process);
void CloseConnectorPort();

View File

@@ -400,31 +400,47 @@ class FileSystemAccessPermissionContext::PermissionGrantImpl
}
}
// Updates the in-memory permission grant for the `new_path` in the `grants`
// map using the same grant from the `old_path`, and removes the grant entry
// for the `old_path`.
// If `allow_overwrite` is true, this will replace any pre-existing grant at
// `new_path`.
static void UpdateGrantPath(
std::map<base::FilePath, PermissionGrantImpl*>& grants,
const content::PathInfo& old_path,
const content::PathInfo& new_path) {
const content::PathInfo& new_path,
bool allow_overwrite) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
auto entry_it =
auto old_path_it =
std::ranges::find_if(grants, [&old_path](const auto& entry) {
return entry.first == old_path.path;
});
if (entry_it == grants.end()) {
// There must be an entry for an ancestor of this entry. Nothing to do
// here.
if (old_path_it == grants.end()) {
return;
}
DCHECK_EQ(entry_it->second->GetActivePermissionStatus(),
DCHECK_EQ(old_path_it->second->GetActivePermissionStatus(),
PermissionStatus::GRANTED);
auto* const grant_impl = entry_it->second;
grant_impl->SetPath(new_path);
auto* const grant_to_move = old_path_it->second;
// Update the permission grant's key in the map of active permissions.
grants.erase(entry_it);
grants.emplace(new_path.path, grant_impl);
// See https://chromium-review.googlesource.com/4803165
if (allow_overwrite) {
auto new_path_it = grants.find(new_path.path);
if (new_path_it != grants.end() && new_path_it->second != grant_to_move) {
new_path_it->second->SetStatus(PermissionStatus::DENIED);
}
}
grant_to_move->SetPath(new_path);
grants.erase(old_path_it);
if (allow_overwrite) {
grants.insert_or_assign(new_path.path, grant_to_move);
} else {
grants.emplace(new_path.path, grant_to_move);
}
}
protected:
@@ -930,12 +946,17 @@ void FileSystemAccessPermissionContext::NotifyEntryMoved(
return;
}
// It's possible `new_path` already has existing persistent permission.
// See crbug.com/423663220.
bool allow_overwrite = base::FeatureList::IsEnabled(
features::kFileSystemAccessMoveWithOverwrite);
auto it = active_permissions_map_.find(origin);
if (it != active_permissions_map_.end()) {
PermissionGrantImpl::UpdateGrantPath(it->second.write_grants, old_path,
new_path);
new_path, allow_overwrite);
PermissionGrantImpl::UpdateGrantPath(it->second.read_grants, old_path,
new_path);
new_path, allow_overwrite);
}
}

View File

@@ -109,7 +109,14 @@ static NSDictionary* UNNotificationResponseToNSDictionary(
}
- (NSMenu*)applicationDockMenu:(NSApplication*)sender {
return menu_controller_ ? menu_controller_.menu : nil;
if (!menu_controller_)
return nil;
// Manually refresh menu state since menuWillOpen: is not called
// by macOS for dock menus for some reason before they are displayed.
NSMenu* menu = menu_controller_.menu;
[menu_controller_ refreshMenuTree:menu];
return menu;
}
- (BOOL)application:(NSApplication*)sender openFile:(NSString*)filename {

View File

@@ -50,6 +50,8 @@
<string>This app needs access to the microphone</string>
<key>NSCameraUsageDescription</key>
<string>This app needs access to the camera</string>
<key>NSAudioCaptureUsageDescription</key>
<string>This app needs access to audio capture</string>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app needs access to Bluetooth</string>
<key>NSBluetoothPeripheralUsageDescription</key>

View File

@@ -57,6 +57,10 @@ class ElectronMenuModel;
// Whether the menu is currently open.
- (BOOL)isMenuOpen;
// Recursively refreshes the menu tree starting from |menu|, applying the
// model state (enabled, checked, hidden etc) to each menu item.
- (void)refreshMenuTree:(NSMenu*)menu;
// NSMenuDelegate methods this class implements. Subclasses should call super
// if extending the behavior.
- (void)menuWillOpen:(NSMenu*)menu;

View File

@@ -493,8 +493,6 @@ NSArray* ConvertSharingItemToNS(const SharingItem& item) {
: NSControlStateValueOff;
}
// Recursively refreshes the menu tree starting from |menu|, applying the
// model state to each menu item.
- (void)refreshMenuTree:(NSMenu*)menu {
for (NSMenuItem* item in menu.itemArray) {
[self applyStateToMenuItem:item];

View File

@@ -150,6 +150,31 @@ bool ElectronDesktopWindowTreeHostWin::HandleMouseEvent(ui::MouseEvent* event) {
return views::DesktopWindowTreeHostWin::HandleMouseEvent(event);
}
bool ElectronDesktopWindowTreeHostWin::HandleIMEMessage(UINT message,
WPARAM w_param,
LPARAM l_param,
LRESULT* result) {
if ((message == WM_SYSCHAR) && (w_param == VK_SPACE)) {
if (native_window_view_->widget() &&
native_window_view_->widget()->non_client_view()) {
const auto* frame =
native_window_view_->widget()->non_client_view()->frame_view();
auto location = frame->GetSystemMenuScreenPixelLocation();
bool prevent_default = false;
native_window_view_->NotifyWindowSystemContextMenu(
location.x(), location.y(), &prevent_default);
return prevent_default ||
views::DesktopWindowTreeHostWin::HandleIMEMessage(message, w_param,
l_param, result);
}
}
return views::DesktopWindowTreeHostWin::HandleIMEMessage(message, w_param,
l_param, result);
}
void ElectronDesktopWindowTreeHostWin::HandleVisibilityChanged(bool visible) {
if (native_window_view_->widget())
native_window_view_->widget()->OnNativeWidgetVisibilityChanged(visible);

View File

@@ -44,6 +44,10 @@ class ElectronDesktopWindowTreeHostWin : public views::DesktopWindowTreeHostWin,
int frame_thickness) const override;
bool HandleMouseEventForCaption(UINT message) const override;
bool HandleMouseEvent(ui::MouseEvent* event) override;
bool HandleIMEMessage(UINT message,
WPARAM w_param,
LPARAM l_param,
LRESULT* result) override;
void HandleVisibilityChanged(bool visible) override;
void SetAllowScreenshots(bool allow) override;

View File

@@ -106,7 +106,10 @@ bool ProcessSignatureIsSameWithCurrentApp(pid_t pid) {
status = SecCodeCheckValidity(process_code.get(), kSecCSDefaultFlags,
self_requirement.get());
if (status != errSecSuccess && status != errSecCSReqFailed) {
OSSTATUS_LOG(ERROR, status) << "SecCodeCheckValidity";
// If the code is unsigned, don't log that (it's not an actual error).
if (status != errSecCSUnsigned) {
OSSTATUS_LOG(ERROR, status) << "SecCodeCheckValidity";
}
return false;
}
return status == errSecSuccess;

View File

@@ -9,6 +9,7 @@ import * as cp from 'node:child_process';
import * as fs from 'node:fs';
import * as http from 'node:http';
import { AddressInfo } from 'node:net';
import * as os from 'node:os';
import * as path from 'node:path';
import { copyMacOSFixtureApp, getCodesignIdentity, shouldRunCodesignTests, signApp, spawn } from './lib/codesign-helpers';
@@ -67,6 +68,38 @@ ifdescribe(shouldRunCodesignTests)('autoUpdater behavior', function () {
}
};
// Squirrel stores update directories in ~/Library/Caches/com.github.Electron.ShipIt/
// as subdirectories named like update.XXXXXXX
const getSquirrelCacheDirectory = () => {
return path.join(os.homedir(), 'Library', 'Caches', 'com.github.Electron.ShipIt');
};
const getUpdateDirectoriesInCache = async () => {
const cacheDir = getSquirrelCacheDirectory();
try {
const entries = await fs.promises.readdir(cacheDir, { withFileTypes: true });
return entries
.filter(entry => entry.isDirectory() && entry.name.startsWith('update.'))
.map(entry => path.join(cacheDir, entry.name));
} catch {
return [];
}
};
const cleanSquirrelCache = async () => {
const cacheDir = getSquirrelCacheDirectory();
try {
const entries = await fs.promises.readdir(cacheDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && entry.name.startsWith('update.')) {
await fs.promises.rm(path.join(cacheDir, entry.name), { recursive: true, force: true });
}
}
} catch {
// Cache dir may not exist yet
}
};
const cachedZips: Record<string, string> = {};
type Mutation = {
@@ -340,6 +373,67 @@ ifdescribe(shouldRunCodesignTests)('autoUpdater behavior', function () {
});
});
it('should clean up old staged update directories when a new update is downloaded', async () => {
// Clean up any existing update directories before the test
await cleanSquirrelCache();
await withUpdatableApp({
nextVersion: '2.0.0',
startFixture: 'update-stack',
endFixture: 'update-stack'
}, async (appPath, updateZipPath2) => {
await withUpdatableApp({
nextVersion: '3.0.0',
startFixture: 'update-stack',
endFixture: 'update-stack'
}, async (_, updateZipPath3) => {
let updateCount = 0;
let downloadCount = 0;
let directoriesDuringSecondDownload: string[] = [];
server.get('/update-file', async (req, res) => {
downloadCount++;
// When the second download request arrives, Squirrel has already
// called uniqueTemporaryDirectoryForUpdate which (with our patch)
// cleans up old directories before creating the new one.
// Without the patch, both directories would exist at this point.
if (downloadCount === 2) {
directoriesDuringSecondDownload = await getUpdateDirectoriesInCache();
}
res.download(updateCount > 1 ? updateZipPath3 : updateZipPath2);
});
server.get('/update-check', (req, res) => {
updateCount++;
res.json({
url: `http://localhost:${port}/update-file`,
name: 'My Release Name',
notes: 'Theses are some release notes innit',
pub_date: (new Date()).toString()
});
});
const relaunchPromise = new Promise<void>((resolve) => {
server.get('/update-check/updated/:version', (req, res) => {
res.status(204).send();
resolve();
});
});
const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
logOnError(launchResult, () => {
expect(launchResult).to.have.property('code', 0);
expect(launchResult.out).to.include('Update Downloaded');
});
await relaunchPromise;
// During the second download, the old staged update directory should
// have been cleaned up. With our patch, there should be exactly 1
// directory (the new one). Without the patch, there would be 2.
expect(directoriesDuringSecondDownload).to.have.lengthOf(1,
`Expected 1 update directory during second download but found ${directoriesDuringSecondDownload.length}: ${directoriesDuringSecondDownload.join(', ')}`);
});
});
});
it('should update to lower version numbers', async () => {
await withUpdatableApp({
nextVersion: '0.0.1',

View File

@@ -43,6 +43,65 @@ describe('MenuItems', () => {
expect(item).to.have.property('role').that.is.a('string');
expect(item).to.have.property('icon');
});
it('should have a default accelerator for certain roles', () => {
const items: Record<string, Electron.MenuItem['accelerator']> = {
undo: 'CommandOrControl+Z',
redo: process.platform === 'win32' ? 'Control+Y' : 'Shift+CommandOrControl+Z',
cut: 'CommandOrControl+X',
copy: 'CommandOrControl+C',
paste: 'CommandOrControl+V',
pasteAndMatchStyle: process.platform === 'darwin' ? 'Cmd+Option+Shift+V' : 'Shift+CommandOrControl+V',
delete: null,
selectAll: 'CommandOrControl+A',
reload: 'CmdOrCtrl+R',
forceReload: 'Shift+CmdOrCtrl+R',
toggleDevTools: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
resetZoom: 'CommandOrControl+0',
zoomIn: 'CommandOrControl+Plus',
zoomOut: 'CommandOrControl+-',
toggleSpellChecker: null,
togglefullscreen: process.platform === 'darwin' ? 'Control+Command+F' : 'F11',
window: null,
minimize: 'CommandOrControl+M',
close: 'CommandOrControl+W',
help: null,
about: null,
services: null,
hide: 'Command+H',
hideOthers: 'Command+Alt+H',
unhide: null,
quit: process.platform === 'win32' ? null : 'CommandOrControl+Q',
showSubstitutions: null,
toggleSmartQuotes: null,
toggleSmartDashes: null,
toggleTextReplacement: null,
startSpeaking: null,
stopSpeaking: null,
zoom: null,
front: null,
appMenu: null,
fileMenu: null,
editMenu: null,
viewMenu: null,
shareMenu: null,
recentDocuments: null,
toggleTabBar: null,
selectNextTab: null,
selectPreviousTab: null,
showAllTabs: null,
mergeAllWindows: null,
clearRecentDocuments: null,
moveTabToNewWindow: null,
windowMenu: null
};
for (const role in items) {
if (!Object.hasOwn(items, role)) continue;
const item = new MenuItem({ role: role as any });
expect(item.accelerator).to.equal(items[role]);
}
});
});
describe('MenuItem.click', () => {
@@ -480,7 +539,7 @@ describe('MenuItems', () => {
it('should display modifiers correctly for simple keys', () => {
const menu = Menu.buildFromTemplate([
{ label: 'text', accelerator: 'CmdOrCtrl+A' },
{ label: 'text', accelerator: 'CommandOrControl+A' },
{ label: 'text', accelerator: 'Shift+A' },
{ label: 'text', accelerator: 'Alt+A' }
]);
@@ -492,7 +551,7 @@ describe('MenuItems', () => {
it('should display modifiers correctly for special keys', () => {
const menu = Menu.buildFromTemplate([
{ label: 'text', accelerator: 'CmdOrCtrl+Tab' },
{ label: 'text', accelerator: 'CommandOrControl+Tab' },
{ label: 'text', accelerator: 'Shift+Tab' },
{ label: 'text', accelerator: 'Alt+Tab' }
]);

View File

@@ -863,5 +863,24 @@ describe('utilityProcess module', () => {
await exit;
}
});
// Note: This doesn't test that disclaiming works (that requires stubbing / mocking TCC which is
// just straight up not possible generically). This just tests that utility processes still launch
// when disclaimed.
ifit(process.platform === 'darwin')('supports disclaim option on macOS', async () => {
const child = utilityProcess.fork(path.join(fixturesPath, 'post-message.js'), [], {
disclaim: true
});
await once(child, 'spawn');
expect(child.pid).to.be.a('number');
// Verify the process can communicate normally
const testMessage = 'test-disclaim';
child.postMessage(testMessage);
const [data] = await once(child, 'message');
expect(data).to.equal(testMessage);
const exit = once(child, 'exit');
expect(child.kill()).to.be.true();
await exit;
});
});
});

View File

@@ -490,6 +490,7 @@ __metadata:
webpack: "npm:^5.95.0"
webpack-cli: "npm:^5.1.4"
wrapper-webpack-plugin: "npm:^2.2.0"
yaml: "npm:^2.8.1"
dependenciesMeta:
abstract-socket:
built: true
@@ -8704,7 +8705,7 @@ __metadata:
languageName: node
linkType: hard
"micromark-core-commonmark@npm:2.0.3":
"micromark-core-commonmark@npm:2.0.3, micromark-core-commonmark@npm:^2.0.0":
version: 2.0.3
resolution: "micromark-core-commonmark@npm:2.0.3"
dependencies:
@@ -8728,30 +8729,6 @@ __metadata:
languageName: node
linkType: hard
"micromark-core-commonmark@npm:^2.0.0":
version: 2.0.1
resolution: "micromark-core-commonmark@npm:2.0.1"
dependencies:
decode-named-character-reference: "npm:^1.0.0"
devlop: "npm:^1.0.0"
micromark-factory-destination: "npm:^2.0.0"
micromark-factory-label: "npm:^2.0.0"
micromark-factory-space: "npm:^2.0.0"
micromark-factory-title: "npm:^2.0.0"
micromark-factory-whitespace: "npm:^2.0.0"
micromark-util-character: "npm:^2.0.0"
micromark-util-chunked: "npm:^2.0.0"
micromark-util-classify-character: "npm:^2.0.0"
micromark-util-html-tag-name: "npm:^2.0.0"
micromark-util-normalize-identifier: "npm:^2.0.0"
micromark-util-resolve-all: "npm:^2.0.0"
micromark-util-subtokenize: "npm:^2.0.0"
micromark-util-symbol: "npm:^2.0.0"
micromark-util-types: "npm:^2.0.0"
checksum: 10c0/a0b280b1b6132f600518e72cb29a4dd1b2175b85f5ed5b25d2c5695e42b876b045971370daacbcfc6b4ce8cf7acbf78dd3a0284528fb422b450144f4b3bebe19
languageName: node
linkType: hard
"micromark-extension-directive@npm:4.0.0":
version: 4.0.0
resolution: "micromark-extension-directive@npm:4.0.0"
@@ -14492,6 +14469,15 @@ __metadata:
languageName: node
linkType: hard
"yaml@npm:^2.8.1":
version: 2.8.2
resolution: "yaml@npm:2.8.2"
bin:
yaml: bin.mjs
checksum: 10c0/703e4dc1e34b324aa66876d63618dcacb9ed49f7e7fe9b70f1e703645be8d640f68ab84f12b86df8ac960bac37acf5513e115de7c970940617ce0343c8c9cd96
languageName: node
linkType: hard
"yamux-js@npm:0.1.2":
version: 0.1.2
resolution: "yamux-js@npm:0.1.2"