mirror of
https://github.com/electron/electron.git
synced 2026-02-26 03:01:17 -05:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a42df2b63 | ||
|
|
47e54e38de | ||
|
|
6ad3726bfa | ||
|
|
a9c0dc5d6c | ||
|
|
6b0aef1d59 | ||
|
|
e54b5eec81 | ||
|
|
79f164a724 | ||
|
|
0bcaabff0c | ||
|
|
2ffb9e1e05 | ||
|
|
02e6c95754 | ||
|
|
46e18e8263 | ||
|
|
4b6eb69fd0 | ||
|
|
a229dbf7a5 | ||
|
|
cad033849b | ||
|
|
16fc71f561 | ||
|
|
f521b01eb8 | ||
|
|
1a9c08914b | ||
|
|
0aba4a6ab8 | ||
|
|
c49899af4c | ||
|
|
92ef86b64a | ||
|
|
30d8e1834c | ||
|
|
b35f4eeaf0 | ||
|
|
63f7692da5 | ||
|
|
a4af2354dc | ||
|
|
bd49864f1d | ||
|
|
abc5d1280d | ||
|
|
712bafde02 | ||
|
|
013768c429 | ||
|
|
681a2c1aba | ||
|
|
98521d22ec | ||
|
|
faa40332ad | ||
|
|
e1ac9d5d1b | ||
|
|
dc254e4bfc | ||
|
|
5631882390 | ||
|
|
e6b53033dd | ||
|
|
fd4f835f37 | ||
|
|
f36da597ff |
6
.github/actions/build-electron/action.yml
vendored
6
.github/actions/build-electron/action.yml
vendored
@@ -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
|
||||
|
||||
@@ -15,7 +15,7 @@ runs:
|
||||
git config --global core.preloadindex true
|
||||
git config --global core.longpaths true
|
||||
fi
|
||||
export BUILD_TOOLS_SHA=4430e4a505e0f4fa2a41b707a10a36f780bbdd26
|
||||
export BUILD_TOOLS_SHA=a0cc95a1884a631559bcca0c948465b725d9295a
|
||||
npm i -g @electron/build-tools
|
||||
# Update depot_tools to ensure python
|
||||
e d update_depot_tools
|
||||
|
||||
122
.github/copilot-instructions.md
vendored
Normal file
122
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,122 @@
|
||||
# Copilot Instructions for Electron
|
||||
|
||||
## Build System
|
||||
|
||||
Electron uses `@electron/build-tools` (`e` CLI). Install with `npm i -g @electron/build-tools`.
|
||||
|
||||
```bash
|
||||
e sync # Fetch sources and apply patches
|
||||
e build # Build Electron (GN + Ninja)
|
||||
e build -k 999 # Build, continuing through errors
|
||||
e start # Run built Electron
|
||||
e start --version # Verify Electron launches
|
||||
e test # Run full test suite
|
||||
e debug # Run in debugger (lldb on macOS, gdb on Linux)
|
||||
```
|
||||
|
||||
### Linting
|
||||
|
||||
```bash
|
||||
npm run lint # Run all linters (JS, C++, Python, GN, docs)
|
||||
npm run lint:js # JavaScript/TypeScript only
|
||||
npm run lint:clang-format # C++ formatting only
|
||||
npm run lint:cpp # C++ linting only
|
||||
npm run lint:docs # Documentation only
|
||||
```
|
||||
|
||||
### Running a Single Test
|
||||
|
||||
```bash
|
||||
npm run test -- -g "pattern" # Run tests matching a regex pattern
|
||||
# Example: npm run test -- -g "ipc"
|
||||
```
|
||||
|
||||
### Running a Single Node.js Test
|
||||
|
||||
```bash
|
||||
node script/node-spec-runner.js parallel/test-crypto-keygen
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
Electron embeds Chromium (rendering) and Node.js (backend) to enable desktop apps with web technologies. The parent directory (`../`) is the Chromium source tree.
|
||||
|
||||
### Process Model
|
||||
|
||||
Electron has two primary process types, mirroring Chromium:
|
||||
|
||||
- **Main process** (`shell/browser/` + `lib/browser/`): Controls app lifecycle, creates windows, system APIs
|
||||
- **Renderer process** (`shell/renderer/` + `lib/renderer/`): Runs web content in BrowserWindows
|
||||
|
||||
### Native ↔ JavaScript Bridge
|
||||
|
||||
Each API is implemented as a C++/JS pair:
|
||||
|
||||
- C++ side: `shell/browser/api/electron_api_{name}.cc/.h` — uses `gin::Wrappable` and `ObjectTemplateBuilder`
|
||||
- JS side: `lib/browser/api/{name}.ts` — exports the module, registered in `lib/browser/api/module-list.ts`
|
||||
- Binding: `NODE_LINKED_BINDING_CONTEXT_AWARE(electron_browser_{name}, Initialize)` in C++ and registered in `shell/common/node_bindings.cc`
|
||||
- Type declaration: `typings/internal-ambient.d.ts` maps `process._linkedBinding('electron_browser_{name}')`
|
||||
|
||||
### Patches System
|
||||
|
||||
Electron patches upstream dependencies (Chromium, Node.js, V8, etc.) rather than forking them. Patches live in `patches/` organized by target, with `patches/config.json` mapping directories to repos.
|
||||
|
||||
```text
|
||||
patches/{target}/*.patch → [e sync] → target repo commits
|
||||
← [e patches] ←
|
||||
```
|
||||
|
||||
Key rules:
|
||||
|
||||
- Fix existing patches rather than creating new ones
|
||||
- Preserve original authorship in TODO comments — never change `TODO(name)` assignees
|
||||
- Each patch commit message must explain why the patch exists
|
||||
- After modifying patches, run `e patches {target}` to export
|
||||
|
||||
When working on the `roller/chromium/main` branch for Chromium upgrades, use `e sync --3` for 3-way merge conflict resolution.
|
||||
|
||||
## Conventions
|
||||
|
||||
### File Naming
|
||||
|
||||
- JS/TS files: kebab-case (`file-name.ts`)
|
||||
- C++ files: snake_case with `electron_api_` prefix (`electron_api_safe_storage.cc`)
|
||||
- Test files: `api-{module-name}-spec.ts` in `spec/`
|
||||
- Source file lists are maintained in `filenames.gni` (with platform-specific sections)
|
||||
|
||||
### JavaScript/TypeScript
|
||||
|
||||
- Semicolons required (`"semi": ["error", "always"]`)
|
||||
- `const` and `let` only (no `var`)
|
||||
- Arrow functions preferred
|
||||
- Import order enforced: `@electron/internal` → `@electron` → `electron` → external → builtin → relative
|
||||
- API naming: `PascalCase` for classes (`BrowserWindow`), `camelCase` for module APIs (`globalShortcut`)
|
||||
- Prefer getters/setters over jQuery-style `.text([text])` patterns
|
||||
|
||||
### C++
|
||||
|
||||
- Follows Chromium coding style, enforced by `clang-format` and `clang-tidy`
|
||||
- Uses Chromium abstractions (`base::`, `content::`, etc.)
|
||||
- Header guards: `#ifndef ELECTRON_SHELL_BROWSER_API_ELECTRON_API_{NAME}_H_`
|
||||
- Platform-specific files: `_mac.mm`, `_win.cc`, `_linux.cc`
|
||||
|
||||
### Testing
|
||||
|
||||
- Framework: Mocha + Chai + Sinon
|
||||
- Test helpers in `spec/lib/` (e.g., `spec-helpers.ts`, `window-helpers.ts`)
|
||||
- Use `defer()` from spec-helpers for cleanup, `closeAllWindows()` for window teardown
|
||||
- Tests import from `electron/main` or `electron/renderer`
|
||||
|
||||
### Documentation
|
||||
|
||||
- API docs in `docs/api/` as Markdown, parsed by `@electron/docs-parser` to generate `electron.d.ts`
|
||||
- API history tracked via YAML blocks in HTML comments within doc files
|
||||
- Docs must pass `npm run lint:docs`
|
||||
|
||||
### Build Configuration
|
||||
|
||||
- `BUILD.gn`: Main GN build config
|
||||
- `buildflags/buildflags.gni`: Feature flags (PDF viewer, extensions, spellchecker)
|
||||
- `build/args/`: Build argument profiles (`testing.gn`, `release.gn`, `all.gn`)
|
||||
- `DEPS`: Dependency versions and checkout paths
|
||||
- `chromium_src/`: Chromium source file overrides (compiled instead of originals)
|
||||
16
.github/problem-matchers/markdownlint.json
vendored
Normal file
16
.github/problem-matchers/markdownlint.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"problemMatcher": [
|
||||
{
|
||||
"owner": "markdownlint",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "^(.+):(\\d+):(\\d+)\\s+(.*)$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"column": 3,
|
||||
"message": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
73
.github/workflows/apply-patches.yml
vendored
Normal file
73
.github/workflows/apply-patches.yml
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
name: Apply Patches
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: apply-patches-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
if: github.repository == 'electron/electron'
|
||||
runs-on: ubuntu-slim
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
outputs:
|
||||
has-patches: ${{ steps.filter.outputs.patches }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
# Use dorny/paths-filter instead of the path filter under the on: pull_request: block
|
||||
# so that the output can be used to conditionally run the apply-patches job, which lets
|
||||
# the job be marked as a required status check (conditional skip counts as a success).
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
patches:
|
||||
- DEPS
|
||||
- 'patches/**'
|
||||
|
||||
apply-patches:
|
||||
needs: setup
|
||||
if: ${{ needs.setup.outputs.has-patches == 'true' }}
|
||||
runs-on: electron-arc-centralus-linux-amd64-32core
|
||||
permissions:
|
||||
contents: read
|
||||
container:
|
||||
image: ghcr.io/electron/build:a82b87d7a4f5ff0cab61405f8151ac4cf4942aeb
|
||||
options: --user root
|
||||
volumes:
|
||||
- /mnt/cross-instance-cache:/mnt/cross-instance-cache
|
||||
- /var/run/sas:/var/run/sas
|
||||
env:
|
||||
CHROMIUM_GIT_COOKIE: ${{ secrets.CHROMIUM_GIT_COOKIE }}
|
||||
GCLIENT_EXTRA_ARGS: '--custom-var=checkout_arm=True --custom-var=checkout_arm64=True'
|
||||
steps:
|
||||
- name: Checkout Electron
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
with:
|
||||
path: src/electron
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
ref: ${{ github.event.pull_request.base.ref }}
|
||||
- name: Merge PR HEAD
|
||||
working-directory: src/electron
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
git config user.email "electron@github.com"
|
||||
git config user.name "Electron Bot"
|
||||
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:
|
||||
target-platform: linux
|
||||
39
.github/workflows/build.yml
vendored
39
.github/workflows/build.yml
vendored
@@ -56,7 +56,6 @@ jobs:
|
||||
src: ${{ steps.filter.outputs.src }}
|
||||
build-image-sha: ${{ steps.set-output.outputs.build-image-sha }}
|
||||
docs-only: ${{ steps.set-output.outputs.docs-only }}
|
||||
has-patches: ${{ steps.filter.outputs.patches }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.0.2
|
||||
with:
|
||||
@@ -73,9 +72,6 @@ jobs:
|
||||
- CODE_OF_CONDUCT.md
|
||||
src:
|
||||
- '!docs/**'
|
||||
patches:
|
||||
- DEPS
|
||||
- 'patches/**'
|
||||
- name: Set Outputs for Build Image SHA & Docs Only
|
||||
id: set-output
|
||||
run: |
|
||||
@@ -108,41 +104,6 @@ jobs:
|
||||
container: '{"image":"ghcr.io/electron/build:${{ needs.checkout-linux.outputs.build-image-sha }}","options":"--user root","volumes":["/mnt/cross-instance-cache:/mnt/cross-instance-cache"]}'
|
||||
secrets: inherit
|
||||
|
||||
# Apply Patches Job
|
||||
apply-patches:
|
||||
needs: setup
|
||||
if: ${{ needs.setup.outputs.has-patches == 'true' }}
|
||||
runs-on: electron-arc-centralus-linux-amd64-32core
|
||||
permissions:
|
||||
contents: read
|
||||
container:
|
||||
image: ghcr.io/electron/build:${{ needs.setup.outputs.build-image-sha }}
|
||||
options: --user root
|
||||
volumes:
|
||||
- /mnt/cross-instance-cache:/mnt/cross-instance-cache
|
||||
- /var/run/sas:/var/run/sas
|
||||
env:
|
||||
CHROMIUM_GIT_COOKIE: ${{ secrets.CHROMIUM_GIT_COOKIE }}
|
||||
GCLIENT_EXTRA_ARGS: '--custom-var=checkout_arm=True --custom-var=checkout_arm64=True'
|
||||
steps:
|
||||
- name: Checkout Electron
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
with:
|
||||
path: src/electron
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Rebase onto Base Branch
|
||||
working-directory: src/electron
|
||||
run: |
|
||||
git config user.email "electron@github.com"
|
||||
git config user.name "Electron Bot"
|
||||
git fetch origin ${{ github.event.pull_request.base.ref }}
|
||||
git rebase origin/${{ github.event.pull_request.base.ref }}
|
||||
- name: Checkout & Sync & Save
|
||||
uses: ./src/electron/.github/actions/checkout
|
||||
with:
|
||||
target-platform: linux
|
||||
|
||||
# Checkout Jobs
|
||||
checkout-macos:
|
||||
needs: setup
|
||||
|
||||
15
.github/workflows/linux-publish.yml
vendored
15
.github/workflows/linux-publish.yml
vendored
@@ -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
|
||||
|
||||
20
.github/workflows/macos-publish.yml
vendored
20
.github/workflows/macos-publish.yml
vendored
@@ -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
|
||||
|
||||
12
.github/workflows/pipeline-electron-lint.yml
vendored
12
.github/workflows/pipeline-electron-lint.yml
vendored
@@ -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
|
||||
|
||||
237
.github/workflows/pipeline-segment-electron-publish.yml
vendored
Normal file
237
.github/workflows/pipeline-segment-electron-publish.yml
vendored
Normal 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)
|
||||
28
.github/workflows/rerun-apply-patches.yml
vendored
28
.github/workflows/rerun-apply-patches.yml
vendored
@@ -15,8 +15,9 @@ jobs:
|
||||
rerun-apply-patches:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write
|
||||
checks: read
|
||||
contents: read
|
||||
pull-requests: read
|
||||
steps:
|
||||
- name: Find PRs and Rerun Apply Patches
|
||||
@@ -33,21 +34,21 @@ jobs:
|
||||
PR_NUMBER=$(echo "$pr" | jq -r '.number')
|
||||
echo "Processing PR #${PR_NUMBER}"
|
||||
|
||||
# Find the apply-patches job check for this PR
|
||||
CHECK=$(gh pr checks "$PR_NUMBER" --json link,name,state,workflow --jq '[.[] | select(.workflow == "Build" and .name == "apply-patches")] | first')
|
||||
# Find the Apply Patches workflow check for this PR
|
||||
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 job found for PR #${PR_NUMBER}"
|
||||
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)
|
||||
@@ -57,13 +58,14 @@ jobs:
|
||||
continue
|
||||
fi
|
||||
|
||||
# Get the job database ID for the apply-patches job
|
||||
JOB_ID=$(gh run view "$RUN_ID" --json jobs --jq '.jobs[] | select(.name == "apply-patches") | .databaseId')
|
||||
# Check if the workflow is currently in progress
|
||||
RUN_STATUS=$(gh run view "$RUN_ID" --json status --jq '.status')
|
||||
|
||||
if [ -z "$JOB_ID" ]; then
|
||||
echo " Could not find apply-patches job ID for run: ${RUN_ID}"
|
||||
continue
|
||||
if [ "$RUN_STATUS" = "in_progress" ] || [ "$RUN_STATUS" = "queued" ] || [ "$RUN_STATUS" = "waiting" ]; then
|
||||
echo " Workflow run ${RUN_ID} is ${RUN_STATUS}, cancelling..."
|
||||
gh run cancel "$RUN_ID" --force
|
||||
gh run watch "$RUN_ID"
|
||||
fi
|
||||
|
||||
gh run rerun "$RUN_ID" --job "$JOB_ID"
|
||||
gh run rerun "$RUN_ID"
|
||||
done
|
||||
|
||||
15
.github/workflows/windows-publish.yml
vendored
15
.github/workflows/windows-publish.yml
vendored
@@ -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
|
||||
|
||||
@@ -250,7 +250,9 @@ Returns:
|
||||
|
||||
Emitted when the user clicks the native macOS new tab button. The new
|
||||
tab button is only visible if the current `BrowserWindow` has a
|
||||
`tabbingIdentifier`
|
||||
`tabbingIdentifier`.
|
||||
|
||||
You must create a window in this handler in order for macOS tabbing to work as expected.
|
||||
|
||||
### Event: 'browser-window-blur'
|
||||
|
||||
@@ -1315,7 +1317,7 @@ Returns `boolean` - Whether the current desktop environment is Unity launcher.
|
||||
### `app.getLoginItemSettings([options])` _macOS_ _Windows_
|
||||
|
||||
* `options` Object (optional)
|
||||
* `type` string (optional) _macOS_ - Can be one of `mainAppService`, `agentService`, `daemonService`, or `loginItemService`. Defaults to `mainAppService`. Only available on macOS 13 and up. See [app.setLoginItemSettings](app.md#appsetloginitemsettingssettings-macos-windows) for more information about each type.
|
||||
* `type` string (optional) _macOS_ - Can be `mainAppService`, `agentService`, `daemonService`, or `loginItemService`. Defaults to `mainAppService`. Only available on macOS 13 and up. See [app.setLoginItemSettings](app.md#appsetloginitemsettingssettings-macos-windows) for more information about each type.
|
||||
* `serviceName` string (optional) _macOS_ - The name of the service. Required if `type` is non-default. Only available on macOS 13 and up.
|
||||
* `path` string (optional) _Windows_ - The executable path to compare against. Defaults to `process.execPath`.
|
||||
* `args` string[] (optional) _Windows_ - The command-line arguments to compare against. Defaults to an empty array.
|
||||
@@ -1330,13 +1332,13 @@ Returns `Object`:
|
||||
* `wasOpenedAtLogin` boolean _macOS_ - `true` if the app was opened at login automatically.
|
||||
* `wasOpenedAsHidden` boolean _macOS_ _Deprecated_ - `true` if the app was opened as a hidden login item. This indicates that the app should not open any windows at startup. This setting is not available on [MAS builds][mas-builds] or on macOS 13 and up.
|
||||
* `restoreState` boolean _macOS_ _Deprecated_ - `true` if the app was opened as a login item that should restore the state from the previous session. This indicates that the app should restore the windows that were open the last time the app was closed. This setting is not available on [MAS builds][mas-builds] or on macOS 13 and up.
|
||||
* `status` string _macOS_ - can be one of `not-registered`, `enabled`, `requires-approval`, or `not-found`.
|
||||
* `status` string _macOS_ - can be `not-registered`, `enabled`, `requires-approval`, or `not-found`.
|
||||
* `executableWillLaunchAtLogin` boolean _Windows_ - `true` if app is set to open at login and its run key is not deactivated. This differs from `openAtLogin` as it ignores the `args` option, this property will be true if the given executable would be launched at login with **any** arguments.
|
||||
* `launchItems` Object[] _Windows_
|
||||
* `name` string _Windows_ - name value of a registry entry.
|
||||
* `path` string _Windows_ - The executable to an app that corresponds to a registry entry.
|
||||
* `args` string[] _Windows_ - the command-line arguments to pass to the executable.
|
||||
* `scope` string _Windows_ - one of `user` or `machine`. Indicates whether the registry entry is under `HKEY_CURRENT USER` or `HKEY_LOCAL_MACHINE`.
|
||||
* `scope` string _Windows_ - can be `user` or `machine`. Indicates whether the registry entry is under `HKEY_CURRENT USER` or `HKEY_LOCAL_MACHINE`.
|
||||
* `enabled` boolean _Windows_ - `true` if the app registry key is startup approved and therefore shows as `enabled` in Task Manager and Windows settings.
|
||||
|
||||
### `app.setLoginItemSettings(settings)` _macOS_ _Windows_
|
||||
|
||||
@@ -32,9 +32,19 @@ update process. Apps that need to disable ATS can add the
|
||||
|
||||
### Windows
|
||||
|
||||
On Windows, you have to install your app into a user's machine before you can
|
||||
use the `autoUpdater`, so it is recommended that you use
|
||||
[electron-winstaller][installer-lib] or [Electron Forge's Squirrel.Windows maker][electron-forge-lib] to generate a Windows installer.
|
||||
On Windows, the `autoUpdater` module automatically selects the appropriate update mechanism
|
||||
based on how your app is packaged:
|
||||
|
||||
* **MSIX packages**: If your app is running as an MSIX package (created with [electron-windows-msix][msix-lib] and detected via [`process.windowsStore`](process.md#processwindowsstore-readonly)),
|
||||
the module uses the MSIX updater, which supports direct MSIX file links and JSON update feeds.
|
||||
* **Squirrel.Windows**: For apps installed via traditional installers (created with
|
||||
[electron-winstaller][installer-lib] or [Electron Forge's Squirrel.Windows maker][electron-forge-lib]),
|
||||
the module uses Squirrel.Windows for updates.
|
||||
|
||||
You don't need to configure which updater to use; Electron automatically detects the packaging
|
||||
format and uses the appropriate one.
|
||||
|
||||
#### Squirrel.Windows
|
||||
|
||||
Apps built with Squirrel.Windows will trigger [custom launch events](https://github.com/Squirrel/Squirrel.Windows/blob/51f5e2cb01add79280a53d51e8d0cfa20f8c9f9f/docs/using/custom-squirrel-events-non-cs.md#application-startup-commands)
|
||||
that must be handled by your Electron application to ensure proper setup and teardown.
|
||||
@@ -55,6 +65,14 @@ The installer generated with Squirrel.Windows will create a shortcut icon with a
|
||||
same ID for your app with `app.setAppUserModelId` API, otherwise Windows will
|
||||
not be able to pin your app properly in task bar.
|
||||
|
||||
#### MSIX Packages
|
||||
|
||||
When your app is packaged as an MSIX, the `autoUpdater` module provides additional
|
||||
functionality:
|
||||
|
||||
* Use the `allowAnyVersion` option in `setFeedURL()` to allow updates to older versions (downgrades)
|
||||
* Support for direct MSIX file links or JSON update feeds (similar to Squirrel.Mac format)
|
||||
|
||||
## Events
|
||||
|
||||
The `autoUpdater` object emits the following events:
|
||||
@@ -92,7 +110,7 @@ Returns:
|
||||
|
||||
Emitted when an update has been downloaded.
|
||||
|
||||
On Windows only `releaseName` is available.
|
||||
With Squirrel.Windows only `releaseName` is available.
|
||||
|
||||
> [!NOTE]
|
||||
> It is not strictly necessary to handle this event. A successfully
|
||||
@@ -111,10 +129,12 @@ The `autoUpdater` object has the following methods:
|
||||
### `autoUpdater.setFeedURL(options)`
|
||||
|
||||
* `options` Object
|
||||
* `url` string
|
||||
* `url` string - The update server URL. For _Windows_ MSIX, this can be either a direct link to an MSIX file (e.g., `https://example.com/update.msix`) or a JSON endpoint that returns update information (see the [Squirrel.Mac][squirrel-mac] README for more information).
|
||||
* `headers` Record\<string, string\> (optional) _macOS_ - HTTP request headers.
|
||||
* `serverType` string (optional) _macOS_ - Can be `json` or `default`, see the [Squirrel.Mac][squirrel-mac]
|
||||
README for more information.
|
||||
* `allowAnyVersion` boolean (optional) _Windows_ - If `true`, allows downgrades to older versions for MSIX packages.
|
||||
Defaults to `false`.
|
||||
|
||||
Sets the `url` and initialize the auto updater.
|
||||
|
||||
@@ -151,3 +171,4 @@ closed.
|
||||
[electron-forge-lib]: https://www.electronforge.io/config/makers/squirrel.windows
|
||||
[app-user-model-id]: https://learn.microsoft.com/en-us/windows/win32/shell/appids
|
||||
[event-emitter]: https://nodejs.org/api/events.html#events_class_eventemitter
|
||||
[msix-lib]: https://github.com/electron-userland/electron-windows-msix
|
||||
|
||||
@@ -351,7 +351,11 @@ Emitted when the window has closed a sheet.
|
||||
|
||||
#### Event: 'new-window-for-tab' _macOS_
|
||||
|
||||
Emitted when the native new tab button is clicked.
|
||||
Emitted when the user clicks the native macOS new tab button. The new
|
||||
tab button is only visible if the current `BrowserWindow` has a
|
||||
`tabbingIdentifier`.
|
||||
|
||||
You must create a window in this handler in order for macOS tabbing to work as expected.
|
||||
|
||||
#### Event: 'system-context-menu' _Windows_ _Linux_
|
||||
|
||||
@@ -756,6 +760,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 +976,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
|
||||
|
||||
@@ -435,7 +435,11 @@ Emitted when the window has closed a sheet.
|
||||
|
||||
#### Event: 'new-window-for-tab' _macOS_
|
||||
|
||||
Emitted when the native new tab button is clicked.
|
||||
Emitted when the user clicks the native macOS new tab button. The new
|
||||
tab button is only visible if the current `BrowserWindow` has a
|
||||
`tabbingIdentifier`.
|
||||
|
||||
You must create a window in this handler in order for macOS tabbing to work as expected.
|
||||
|
||||
#### Event: 'system-context-menu' _Windows_ _Linux_
|
||||
|
||||
@@ -862,6 +866,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 +1094,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
|
||||
|
||||
@@ -361,6 +361,13 @@ Keep in mind that standalone switches can sometimes be split into individual fea
|
||||
|
||||
Finally, you'll need to ensure that the version of Chromium in Electron matches the version of the browser you're using to cross-reference the switches.
|
||||
|
||||
### Chromium features relevant to Electron apps
|
||||
|
||||
* `AlwaysLogLOAFURL`: enables script attribution for
|
||||
[`long-animation-frame`](https://developer.mozilla.org/en-US/docs/Web/API/Performance_API/Long_animation_frame_timing)
|
||||
`PerformanceObserver` events for non-http(s), non-data, non-blob URLs (such as `file:` or custom
|
||||
protocol URLs).
|
||||
|
||||
[app]: app.md
|
||||
[append-switch]: command-line.md#commandlineappendswitchswitch-value
|
||||
[debugging-main-process]: ../tutorial/debugging-main-process.md
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -159,6 +159,22 @@ Notification activated (com.github.Electron:notification:EAF7B87C-A113-43D7-8E76
|
||||
Notification replied to (com.github.Electron:notification:EAF7B87C-A113-43D7-8E76-F88EC9D73D44)
|
||||
```
|
||||
|
||||
### `ELECTRON_DEBUG_MSIX_UPDATER`
|
||||
|
||||
Adds extra logs to MSIX updater operations on Windows to aid in debugging. Extra logging will be displayed when MSIX update operations are initiated, including package updates, package registration, and restart registration. This helps diagnose issues with MSIX package updates and deployments.
|
||||
|
||||
Sample output:
|
||||
|
||||
```sh
|
||||
UpdateMsix called with URI: https://example.com/app.msix
|
||||
DoUpdateMsix: Starting
|
||||
Calling AddPackageByUriAsync... URI: https://example.com/app.msix
|
||||
Update options - deferRegistration: true, developerMode: false, forceShutdown: false, forceTargetShutdown: false, forceUpdateFromAnyVersion: false
|
||||
Waiting for deployment...
|
||||
Deployment finished.
|
||||
MSIX Deployment completed.
|
||||
```
|
||||
|
||||
### `ELECTRON_LOG_ASAR_READS`
|
||||
|
||||
When Electron reads from an ASAR file, log the read offset and file path to
|
||||
|
||||
@@ -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_
|
||||
|
||||
|
||||
@@ -128,8 +128,8 @@ A `string` representing Electron's version string.
|
||||
|
||||
### `process.windowsStore` _Readonly_
|
||||
|
||||
A `boolean`. If the app is running as a Windows Store app (appx), this property is `true`,
|
||||
for otherwise it is `undefined`.
|
||||
A `boolean`. If the app is running as an MSIX package (including AppX for Windows Store),
|
||||
this property is `true`, otherwise it is `undefined`.
|
||||
|
||||
### `process.contextId` _Readonly_
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -6,7 +6,7 @@ hide_title: false
|
||||
---
|
||||
|
||||
After creating an [application distribution](application-distribution.md), the
|
||||
app's source code are usually bundled into an [ASAR archive](https://github.com/electron/asar),
|
||||
app's source code is usually bundled into an [ASAR archive](https://github.com/electron/asar),
|
||||
which is a simple extensive archive format designed for Electron apps. By bundling the app
|
||||
we can mitigate issues around long path names on Windows, speed up `require` and conceal your source
|
||||
code from cursory inspection.
|
||||
@@ -134,7 +134,7 @@ underlying system calls, Electron will extract the needed file into a
|
||||
temporary file and pass the path of the temporary file to the APIs to make them
|
||||
work. This adds a little overhead for those APIs.
|
||||
|
||||
APIs that requires extra unpacking are:
|
||||
APIs that require extra unpacking are:
|
||||
|
||||
* `child_process.execFile`
|
||||
* `child_process.execFileSync`
|
||||
|
||||
@@ -24,7 +24,7 @@ All versions of `@electron/asar` support ASAR integrity.
|
||||
## How it works
|
||||
|
||||
Each ASAR archive contains a JSON string header. The header format includes an `integrity` object
|
||||
that contain a hex encoded hash of the entire archive as well as an array of hex encoded hashes for each
|
||||
that contains a hex encoded hash of the entire archive as well as an array of hex encoded hashes for each
|
||||
block of `blockSize` bytes.
|
||||
|
||||
```json
|
||||
|
||||
@@ -203,7 +203,7 @@ test('launch app', async () => {
|
||||
})
|
||||
```
|
||||
|
||||
After that, you will access to an instance of Playwright's `ElectronApp` class. This
|
||||
After that, you will have access to an instance of Playwright's `ElectronApp` class. This
|
||||
is a powerful class that has access to main process modules for example:
|
||||
|
||||
```js {5-10} @ts-nocheck
|
||||
@@ -237,7 +237,7 @@ test('save screenshot', async () => {
|
||||
})
|
||||
```
|
||||
|
||||
Putting all this together using the Playwright test-runner, let's create a `example.spec.js`
|
||||
Putting all this together using the Playwright test-runner, let's create an `example.spec.js`
|
||||
test file with a single test and assertion:
|
||||
|
||||
```js title='example.spec.js' @ts-nocheck
|
||||
@@ -377,7 +377,7 @@ class TestDriver {
|
||||
module.exports = { TestDriver }
|
||||
```
|
||||
|
||||
In your app code, can then write a simple handler to receive RPC calls:
|
||||
In your app code, you can then write a simple handler to receive RPC calls:
|
||||
|
||||
```js title='main.js'
|
||||
const METHODS = {
|
||||
|
||||
@@ -17,7 +17,7 @@ run them, users need to go through multiple advanced and manual steps.
|
||||
|
||||
If you are building an Electron app that you intend to package and distribute,
|
||||
it should be code signed. The Electron ecosystem tooling makes codesigning your
|
||||
apps straightforward - this documentation explains how sign your apps on both
|
||||
apps straightforward - this documentation explains how to sign your apps on both
|
||||
Windows and macOS.
|
||||
|
||||
## Signing & notarizing macOS builds
|
||||
@@ -210,7 +210,7 @@ const msiCreator = new MSICreator({
|
||||
const supportBinaries = await msiCreator.create()
|
||||
|
||||
// 🆕 Step 2a: optionally sign support binaries if you
|
||||
// sign you binaries as part of of your packaging script
|
||||
// sign your binaries as part of your packaging script
|
||||
for (const binary of supportBinaries) {
|
||||
// Binaries are the new stub executable and optionally
|
||||
// the Squirrel auto updater.
|
||||
|
||||
@@ -110,7 +110,7 @@ const win = new BrowserWindow({
|
||||
#### Show and hide the traffic lights programmatically _macOS_
|
||||
|
||||
You can also show and hide the traffic lights programmatically from the main process.
|
||||
The `win.setWindowButtonVisibility` forces traffic lights to be show or hidden depending
|
||||
The `win.setWindowButtonVisibility` forces traffic lights to be shown or hidden depending
|
||||
on the value of its boolean parameter.
|
||||
|
||||
```js title='main.js'
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
By default, windows are dragged using the title bar provided by the OS chrome. Apps
|
||||
that remove the default title bar need to use the `app-region` CSS property to define
|
||||
specific areas that can be used to drag the window. Setting `app-region: drag` marks
|
||||
a rectagular area as draggable.
|
||||
a rectangular area as draggable.
|
||||
|
||||
It is important to note that draggable areas ignore all pointer events. For example,
|
||||
a button element that overlaps a draggable region will not emit mouse clicks or mouse
|
||||
enter/exit events within that overlapping area. Setting `app-region: no-drag` reenables
|
||||
pointer events by excluding a rectagular area from a draggable region.
|
||||
pointer events by excluding a rectangular area from a draggable region.
|
||||
|
||||
To make the whole window draggable, you can add `app-region: drag` as
|
||||
`body`'s style:
|
||||
|
||||
@@ -29,7 +29,7 @@ be updated accordingly.
|
||||
In macOS 10.14 Mojave, Apple introduced a new [system-wide dark mode][system-wide-dark-mode]
|
||||
for all macOS computers. If your Electron app has a dark mode, you can make it
|
||||
follow the system-wide dark mode setting using
|
||||
[the `nativeTheme` api](../api/native-theme.md).
|
||||
[the `nativeTheme` API](../api/native-theme.md).
|
||||
|
||||
In macOS 10.15 Catalina, Apple introduced a new "automatic" dark mode option
|
||||
for all macOS computers. In order for the `nativeTheme.shouldUseDarkColors` and
|
||||
|
||||
@@ -171,7 +171,7 @@ sections.
|
||||
|
||||
In the main process, we'll be creating a `handleFileOpen()` function that calls
|
||||
`dialog.showOpenDialog` and returns the value of the file path selected by the user. This function
|
||||
is used as a callback whenever an `ipcRender.invoke` message is sent through the `dialog:openFile`
|
||||
is used as a callback whenever an `ipcRenderer.invoke` message is sent through the `dialog:openFile`
|
||||
channel from the renderer process. The return value is then returned as a Promise to the original
|
||||
`invoke` call.
|
||||
|
||||
@@ -446,7 +446,7 @@ After loading the preload script, your renderer process should have access to th
|
||||
We don't directly expose the whole `ipcRenderer.on` API for [security reasons][]. Make sure to
|
||||
limit the renderer's access to Electron APIs as much as possible.
|
||||
Also don't just pass the callback to `ipcRenderer.on` as this will leak `ipcRenderer` via `event.sender`.
|
||||
Use a custom handler that invoke the `callback` only with the desired arguments.
|
||||
Use a custom handler that invokes the `callback` only with the desired arguments.
|
||||
:::
|
||||
|
||||
:::info
|
||||
|
||||
@@ -10,7 +10,7 @@ hide_title: false
|
||||
## Accelerators
|
||||
|
||||
Accelerators are strings that can be used to represent keyboard shortcuts throughout your Electron.
|
||||
These strings can contain multiple modifiers keys and a single key code joined by the `+` character.
|
||||
These strings can contain multiple modifier keys and a single key code joined by the `+` character.
|
||||
|
||||
> [!NOTE]
|
||||
> Accelerators are **case-insensitive**.
|
||||
|
||||
@@ -62,9 +62,9 @@ const createWindow = () => {
|
||||
}
|
||||
```
|
||||
|
||||
In this next step, we will create our `BrowserWindow` and tell our application how to handle an event in which an external protocol is clicked.
|
||||
In this next step, we will create our `BrowserWindow` and tell our application how to handle an event in which an external protocol is clicked.
|
||||
|
||||
This code will be different in Windows and Linux compared to MacOS. This is due to both platforms emitting the `second-instance` event rather than the `open-url` event and Windows requiring additional code in order to open the contents of the protocol link within the same Electron instance. Read more about this [here](../api/app.md#apprequestsingleinstancelockadditionaldata).
|
||||
This code will be different in Windows and Linux compared to macOS. This is due to both platforms emitting the `second-instance` event rather than the `open-url` event and Windows requiring additional code in order to open the contents of the protocol link within the same Electron instance. Read more about this [here](../api/app.md#apprequestsingleinstancelockadditionaldata).
|
||||
|
||||
#### Windows and Linux code:
|
||||
|
||||
@@ -91,7 +91,7 @@ if (!gotTheLock) {
|
||||
}
|
||||
```
|
||||
|
||||
#### MacOS code:
|
||||
#### macOS code:
|
||||
|
||||
```js @ts-type={createWindow:()=>void}
|
||||
// This method will be called when Electron has finished
|
||||
|
||||
@@ -65,7 +65,7 @@ The full list of certificate types can be found
|
||||
Apps signed with "Apple Development" and "Apple Distribution" certificates can
|
||||
only run under [App Sandbox][app-sandboxing], so they must use the MAS build of
|
||||
Electron. However, the "Developer ID Application" certificate does not have this
|
||||
restrictions, so apps signed with it can use either the normal build or the MAS
|
||||
restriction, so apps signed with it can use either the normal build or the MAS
|
||||
build of Electron.
|
||||
|
||||
#### Legacy certificate names
|
||||
@@ -208,7 +208,7 @@ signAsync({
|
||||
After signing the app with the "Apple Distribution" certificate, you can
|
||||
continue to submit it to Mac App Store.
|
||||
|
||||
However, this guide do not ensure your app will be approved by Apple; you
|
||||
However, this guide does not ensure your app will be approved by Apple; you
|
||||
still need to read Apple's [Submitting Your App][submitting-your-app] guide on
|
||||
how to meet the Mac App Store requirements.
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ Electron application, and this property only exists on macOS.
|
||||
One of the main uses for your app's Dock icon is to expose additional app menus. The Dock menu is
|
||||
triggered by right-clicking or <kbd>Ctrl</kbd>-clicking the app icon. By default, the app's Dock menu
|
||||
will come with system-provided window management utilities, including the ability to show all windows,
|
||||
hide the app, and switch betweeen different open windows.
|
||||
hide the app, and switch between different open windows.
|
||||
|
||||
To set an app-defined custom Dock menu, pass any [Menu](../api/menu.md) instance into the
|
||||
[`dock.setMenu`](../api/dock.md#docksetmenumenu-macos) API.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1339,7 +1339,7 @@ For developers wanting to learn more, you can refer to the [official N-API docum
|
||||
|
||||
### Putting `cpp_addon.cc` together
|
||||
|
||||
We've now finished the bridge part our addon - that is, the code that's most concerned with being the bridge between your JavaScript and C++ code (and by contrast, less so actually interacting with the operating system or GTK). After adding all the sections above, your `src/cpp_addon.cc` should look like this:
|
||||
We've now finished the bridge part of our addon - that is, the code that's most concerned with being the bridge between your JavaScript and C++ code (and by contrast, less so actually interacting with the operating system or GTK). After adding all the sections above, your `src/cpp_addon.cc` should look like this:
|
||||
|
||||
```cpp title='src/cpp_addon.cc'
|
||||
#include <napi.h>
|
||||
|
||||
@@ -4,13 +4,13 @@ This tutorial builds on the [general introduction to Native Code and Electron](.
|
||||
|
||||
Specifically, we'll be integrating with two commonly used native Windows libraries:
|
||||
|
||||
* `comctl32.lib`, which contains common controls and user interface components. It provides various UI elements like buttons, scrollbars, toolbars, status bars, progress bars, and tree views. As far as GUI development on Windows goes, this library is very low-level and basic - more modern frameworks like WinUI or WPF are advanced and alternatives but require a lot more C++ and Windows version considerations than are useful for this tutorial. This way, we can avoid the many perils of building native interfaces for multiple Windows versions!
|
||||
* `comctl32.lib`, which contains common controls and user interface components. It provides various UI elements like buttons, scrollbars, toolbars, status bars, progress bars, and tree views. As far as GUI development on Windows goes, this library is very low-level and basic - more modern frameworks like WinUI or WPF are more advanced alternatives but require a lot more C++ and Windows version considerations than are useful for this tutorial. This way, we can avoid the many perils of building native interfaces for multiple Windows versions!
|
||||
* `shcore.lib`, a library that provides high-DPI awareness functionality and other Shell-related features around managing displays and UI elements.
|
||||
|
||||
This tutorial will be most useful to those who already have some familiarity with native C++ GUI development on Windows. You should have experience with basic window classes and procedures, like `WNDCLASSEXW` and `WindowProc` functions. You should also be familiar with the Windows message loop, which is the heart of any native application - our code will be using `GetMessage`, `TranslateMessage`, and `DispatchMessage` to handle messages. Lastly, we'll be using (but not explaining) standard Win32 controls like `WC_EDITW` or `WC_BUTTONW`.
|
||||
|
||||
> [!NOTE]
|
||||
> If you're not familiar with C++ GUI development on Windows, we recommend Microsoft's excellent documentation and guides, particular for beginners. "[Get Started with Win32 and C++](https://learn.microsoft.com/en-us/windows/win32/learnwin32/learn-to-program-for-windows)" is a great introduction.
|
||||
> If you're not familiar with C++ GUI development on Windows, we recommend Microsoft's excellent documentation and guides, particularly for beginners. "[Get Started with Win32 and C++](https://learn.microsoft.com/en-us/windows/win32/learnwin32/learn-to-program-for-windows)" is a great introduction.
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -1333,7 +1333,7 @@ npm run build
|
||||
|
||||
## Conclusion
|
||||
|
||||
You've now built a complete native Node.js addon for Windows using C++ and the Win32 API. Some of things we've done here are:
|
||||
You've now built a complete native Node.js addon for Windows using C++ and the Win32 API. Some of the things we've done here are:
|
||||
|
||||
1. Creating a native Windows GUI from C++
|
||||
2. Implementing a Todo list application with Add, Edit, and Delete functionality
|
||||
|
||||
@@ -1167,7 +1167,7 @@ The approach demonstrated here allows you to:
|
||||
* Setting up bidirectional communication using callbacks and events
|
||||
* Configuring a custom build process to compile Swift code
|
||||
|
||||
For more information on developing with Swift and Swift, refer to Apple's developer documentation:
|
||||
For more information on developing with Swift and SwiftUI, refer to Apple's developer documentation:
|
||||
|
||||
* [Swift Programming Language](https://developer.apple.com/swift/)
|
||||
* [SwiftUI Framework](https://developer.apple.com/documentation/swiftui)
|
||||
|
||||
@@ -36,7 +36,7 @@ setting.
|
||||
This is an advanced feature requiring a native node module to work with your own code.
|
||||
The frames are directly copied in GPU textures, thus this mode is very fast because
|
||||
there's no CPU-GPU memory copies overhead, and you can directly import the shared
|
||||
texture to your own rendering program. You can read more details at
|
||||
texture to your own rendering program. You can read more details
|
||||
[here](https://github.com/electron/electron/blob/main/shell/browser/osr/README.md).
|
||||
|
||||
2. Use CPU shared memory bitmap
|
||||
|
||||
@@ -294,7 +294,7 @@ particularly useful if users complain about your app sometimes "stuttering".
|
||||
|
||||
Generally speaking, all advice for building performant web apps for modern
|
||||
browsers apply to Electron's renderers, too. The two primary tools at your
|
||||
disposal are currently `requestIdleCallback()` for small operations and
|
||||
disposal are currently `requestIdleCallback()` for small operations and
|
||||
`Web Workers` for long-running operations.
|
||||
|
||||
_`requestIdleCallback()`_ allows developers to queue up a function to be
|
||||
@@ -360,7 +360,7 @@ turning into a desktop application. As web developers, we are used to loading
|
||||
resources from a variety of content delivery networks. Now that you are
|
||||
shipping a proper desktop application, attempt to "cut the cord" where possible
|
||||
and avoid letting your users wait for resources that never change and could
|
||||
easily be included in your app.
|
||||
easily be included in your app.
|
||||
|
||||
A typical example is Google Fonts. Many developers make use of Google's
|
||||
impressive collection of free fonts, which comes with a content delivery
|
||||
|
||||
@@ -113,7 +113,7 @@ For a full list of Electron's main process modules, check out our API documentat
|
||||
|
||||
Each Electron app spawns a separate renderer process for each open `BrowserWindow`
|
||||
(and each web embed). As its name implies, a renderer is responsible for
|
||||
_rendering_ web content. For all intents and purposes, code ran in renderer processes
|
||||
_rendering_ web content. For all intents and purposes, code run in renderer processes
|
||||
should behave according to web standards (insofar as Chromium does, at least).
|
||||
|
||||
Therefore, all user interfaces and app functionality within a single browser
|
||||
|
||||
@@ -767,7 +767,7 @@ ipcMain.handle('get-secrets', (e) => {
|
||||
})
|
||||
|
||||
function validateSender (frame) {
|
||||
// Value the host of the URL using an actual URL parser and an allowlist
|
||||
// Validate the host of the URL using an actual URL parser and an allowlist
|
||||
if ((new URL(frame.url)).host === 'electronjs.org') return true
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
Being based on Chromium, Electron requires a display driver to function.
|
||||
If Chromium can't find a display driver, Electron will fail to launch -
|
||||
and therefore not executing any of your tests, regardless of how you are running
|
||||
them. Testing Electron-based apps on Travis, CircleCI, Jenkins or similar Systems
|
||||
and therefore not execute any of your tests, regardless of how you are running
|
||||
them. Testing Electron-based apps on Travis, CircleCI, Jenkins or similar systems
|
||||
requires therefore a little bit of configuration. In essence, we need to use
|
||||
a virtual display driver.
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ following JSON format:
|
||||
"updateTo": {
|
||||
"version": "1.2.1",
|
||||
"pub_date": "2023-09-18T12:29:53+01:00",
|
||||
"notes": "Theses are some release notes innit",
|
||||
"notes": "These are some release notes innit",
|
||||
"name": "1.2.1",
|
||||
"url": "https://mycompany.example.com/myapp/releases/myrelease"
|
||||
}
|
||||
@@ -54,7 +54,7 @@ following JSON format:
|
||||
"updateTo": {
|
||||
"version": "1.2.3",
|
||||
"pub_date": "2024-09-18T12:29:53+01:00",
|
||||
"notes": "Theses are some more release notes innit",
|
||||
"notes": "These are some more release notes innit",
|
||||
"name": "1.2.3",
|
||||
"url": "https://mycompany.example.com/myapp/releases/myrelease3"
|
||||
}
|
||||
@@ -307,7 +307,7 @@ app update. All other properties in the object are optional.
|
||||
{
|
||||
"url": "https://your-static.storage/your-app-1.2.3-darwin.zip",
|
||||
"name": "1.2.3",
|
||||
"notes": "Theses are some release notes innit",
|
||||
"notes": "These are some release notes innit",
|
||||
"pub_date": "2024-09-18T12:29:53+01:00"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -149,7 +149,7 @@ for an example delay-load hook if you're implementing your own.
|
||||
native Node modules with prebuilt binaries for multiple versions of Node
|
||||
and Electron.
|
||||
|
||||
If the `prebuild`-powered module provide binaries for the usage in Electron,
|
||||
If the `prebuild`-powered module provides binaries for the usage in Electron,
|
||||
make sure to omit `--build-from-source` and the `npm_config_build_from_source`
|
||||
environment variable in order to take full advantage of the prebuilt binaries.
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ To test your app, use a Windows on Arm device running Windows 10 (version 1903 o
|
||||
|
||||
### Node.js/node-gyp
|
||||
|
||||
[Node.js v12.9.0 or later is recommended.](https://nodejs.org/en/) If updating to a new version of Node is undesirable, you can instead [update npm's copy of node-gyp manually](https://github.com/nodejs/node-gyp/wiki/Updating-npm's-bundled-node-gyp) to version 5.0.2 or later, which contains the required changes to compile native modules for Arm.
|
||||
[Node.js v12.9.0 or later is recommended.](https://nodejs.org/en/) If updating to a new version of Node is undesirable, you can instead [update npm's copy of node-gyp manually](https://github.com/nodejs/node-gyp/wiki/Updating-npm's-bundled-node-gyp) to version 5.0.2 or later, which contains the required changes to compile native modules for Arm.
|
||||
|
||||
### Visual Studio 2017
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ system.
|
||||
|
||||
Before running the CLI for the first time, you will have to setup the "Windows Desktop App
|
||||
Converter". This will take a few minutes, but don't worry - you only have to do
|
||||
this once. Download and Desktop App Converter from [here][app-converter].
|
||||
this once. Download the Desktop App Converter from [here][app-converter].
|
||||
You will receive two files: `DesktopAppConverter.zip` and `BaseImage-14316.wim`.
|
||||
|
||||
1. Unzip `DesktopAppConverter.zip`. From an elevated PowerShell (opened with
|
||||
|
||||
@@ -222,8 +222,10 @@ auto_filenames = {
|
||||
browser_bundle_deps = [
|
||||
"lib/browser/api/app.ts",
|
||||
"lib/browser/api/auto-updater.ts",
|
||||
"lib/browser/api/auto-updater/auto-updater-msix.ts",
|
||||
"lib/browser/api/auto-updater/auto-updater-native.ts",
|
||||
"lib/browser/api/auto-updater/auto-updater-win.ts",
|
||||
"lib/browser/api/auto-updater/msix-update-win.ts",
|
||||
"lib/browser/api/auto-updater/squirrel-update-win.ts",
|
||||
"lib/browser/api/base-window.ts",
|
||||
"lib/browser/api/browser-view.ts",
|
||||
|
||||
@@ -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",
|
||||
@@ -587,6 +589,7 @@ filenames = {
|
||||
"shell/common/electron_command_line.cc",
|
||||
"shell/common/electron_command_line.h",
|
||||
"shell/common/electron_constants.h",
|
||||
"shell/common/electron_paths.cc",
|
||||
"shell/common/electron_paths.h",
|
||||
"shell/common/gin_converters/accelerator_converter.cc",
|
||||
"shell/common/gin_converters/accelerator_converter.h",
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
if (process.platform === 'win32') {
|
||||
module.exports = require('./auto-updater/auto-updater-win');
|
||||
// windowsStore indicates whether the app is running as a packaged app (MSIX), even outside of the store
|
||||
if (process.windowsStore) {
|
||||
module.exports = require('./auto-updater/auto-updater-msix');
|
||||
} else {
|
||||
module.exports = require('./auto-updater/auto-updater-win');
|
||||
}
|
||||
} else {
|
||||
module.exports = require('./auto-updater/auto-updater-native');
|
||||
}
|
||||
|
||||
449
lib/browser/api/auto-updater/auto-updater-msix.ts
Normal file
449
lib/browser/api/auto-updater/auto-updater-msix.ts
Normal file
@@ -0,0 +1,449 @@
|
||||
import * as msixUpdate from '@electron/internal/browser/api/auto-updater/msix-update-win';
|
||||
|
||||
import { app, net } from 'electron/main';
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
interface UpdateInfo {
|
||||
ok: boolean; // False if error encountered
|
||||
available?: boolean; // True if the update is available, false if not
|
||||
updateUrl?: string; // The URL of the update
|
||||
releaseNotes?: string; // The release notes of the update
|
||||
releaseName?: string; // The release name of the update
|
||||
releaseDate?: Date; // The release date of the update
|
||||
}
|
||||
|
||||
interface MSIXPackageInfo {
|
||||
id: string;
|
||||
familyName: string;
|
||||
developmentMode: boolean;
|
||||
version: string;
|
||||
signatureKind: 'developer' | 'enterprise' | 'none' | 'store' | 'system';
|
||||
appInstallerUri?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for updating an MSIX package.
|
||||
* Used with `updateMsix()` to control how the package update behaves.
|
||||
*
|
||||
* These options correspond to the Windows.Management.Deployment.AddPackageOptions class properties.
|
||||
*
|
||||
* @see https://learn.microsoft.com/en-us/uwp/api/windows.management.deployment.addpackageoptions?view=winrt-26100
|
||||
*/
|
||||
export interface UpdateMsixOptions {
|
||||
/**
|
||||
* Gets or sets a value that indicates whether to delay registration of the main package
|
||||
* or dependency packages if the packages are currently in use.
|
||||
*
|
||||
* Corresponds to `AddPackageOptions.DeferRegistrationWhenPackagesAreInUse`
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
deferRegistration?: boolean;
|
||||
|
||||
/**
|
||||
* Gets or sets a value that indicates whether the app is installed in developer mode.
|
||||
* When set, the app is installed in development mode which allows for a more rapid
|
||||
* development cycle. The BlockMap.xml, [Content_Types].xml, and digital signature
|
||||
* files are not required for app installation.
|
||||
*
|
||||
* Corresponds to `AddPackageOptions.DeveloperMode`
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
developerMode?: boolean;
|
||||
|
||||
/**
|
||||
* Gets or sets a value that indicates whether the processes associated with the package
|
||||
* will be shut down forcibly so that registration can continue if the package, or any
|
||||
* package that depends on the package, is currently in use.
|
||||
*
|
||||
* Corresponds to `AddPackageOptions.ForceAppShutdown`
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
forceShutdown?: boolean;
|
||||
|
||||
/**
|
||||
* Gets or sets a value that indicates whether the processes associated with the package
|
||||
* will be shut down forcibly so that registration can continue if the package is
|
||||
* currently in use.
|
||||
*
|
||||
* Corresponds to `AddPackageOptions.ForceTargetAppShutdown`
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
forceTargetShutdown?: boolean;
|
||||
|
||||
/**
|
||||
* Gets or sets a value that indicates whether to force a specific version of a package
|
||||
* to be added, regardless of if a higher version is already added.
|
||||
*
|
||||
* Corresponds to `AddPackageOptions.ForceUpdateFromAnyVersion`
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
forceUpdateFromAnyVersion?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for registering an MSIX package.
|
||||
* Used with `registerPackage()` to control how the package registration behaves.
|
||||
*
|
||||
* These options correspond to the Windows.Management.Deployment.DeploymentOptions enum.
|
||||
*
|
||||
* @see https://learn.microsoft.com/en-us/uwp/api/windows.management.deployment.deploymentoptions?view=winrt-26100
|
||||
*/
|
||||
interface RegisterPackageOptions {
|
||||
/**
|
||||
* Force shutdown of the application if it's currently running.
|
||||
* If this package, or any package that depends on this package, is currently in use,
|
||||
* the processes associated with the package are shut down forcibly so that registration can continue.
|
||||
*
|
||||
* Corresponds to `DeploymentOptions.ForceApplicationShutdown` (value: 1)
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
forceShutdown?: boolean;
|
||||
|
||||
/**
|
||||
* Force shutdown of the target application if it's currently running.
|
||||
* If this package is currently in use, the processes associated with the package
|
||||
* are shut down forcibly so that registration can continue.
|
||||
*
|
||||
* Corresponds to `DeploymentOptions.ForceTargetApplicationShutdown` (value: 64)
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
forceTargetShutdown?: boolean;
|
||||
|
||||
/**
|
||||
* Force a specific version of a package to be staged/registered, regardless of if
|
||||
* a higher version is already staged/registered.
|
||||
*
|
||||
* Corresponds to `DeploymentOptions.ForceUpdateFromAnyVersion` (value: 262144)
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
forceUpdateFromAnyVersion?: boolean;
|
||||
}
|
||||
|
||||
class AutoUpdater extends EventEmitter implements Electron.AutoUpdater {
|
||||
updateAvailable: boolean = false;
|
||||
updateURL: string | null = null;
|
||||
updateHeaders: Record<string, string> | null = null;
|
||||
allowAnyVersion: boolean = false;
|
||||
|
||||
// Private: Validate that the URL points to an MSIX file (following redirects)
|
||||
private async validateMsixUrl (url: string): Promise<void> {
|
||||
try {
|
||||
// Make a HEAD request to follow redirects and get the final URL
|
||||
const response = await net.fetch(url, {
|
||||
method: 'HEAD',
|
||||
headers: this.updateHeaders ? new Headers(this.updateHeaders) : undefined,
|
||||
redirect: 'follow' // Follow redirects to get the final URL
|
||||
});
|
||||
|
||||
// Get the final URL after redirects (response.url contains the final URL)
|
||||
const finalUrl = response.url || url;
|
||||
const urlObj = new URL(finalUrl);
|
||||
const pathname = urlObj.pathname.toLowerCase();
|
||||
|
||||
// Check if final URL ends with .msix or .msixbundle extension
|
||||
const hasMsixExtension = pathname.endsWith('.msix') || pathname.endsWith('.msixbundle');
|
||||
|
||||
if (!hasMsixExtension) {
|
||||
throw new Error(`Update URL does not point to an MSIX file. Expected .msix or .msixbundle extension, got final URL: ${finalUrl}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof TypeError) {
|
||||
throw new Error(`Invalid MSIX URL: ${url}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Private: Check if URL is a direct MSIX file (following redirects)
|
||||
private async isDirectMsixUrl (url: string, emitError: boolean = false): Promise<boolean> {
|
||||
try {
|
||||
await this.validateMsixUrl(url);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (emitError) {
|
||||
this.emitError(error as Error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Supports both versioning (x.y.z) and Windows version format (x.y.z.a)
|
||||
// Returns: 1 if v1 > v2, -1 if v1 < v2, 0 if v1 === v2
|
||||
private compareVersions (v1: string, v2: string): number {
|
||||
const parts1 = v1.split('.').map(part => {
|
||||
const parsed = parseInt(part, 10);
|
||||
return isNaN(parsed) ? 0 : parsed;
|
||||
});
|
||||
const parts2 = v2.split('.').map(part => {
|
||||
const parsed = parseInt(part, 10);
|
||||
return isNaN(parsed) ? 0 : parsed;
|
||||
});
|
||||
|
||||
const maxLength = Math.max(parts1.length, parts2.length);
|
||||
|
||||
for (let i = 0; i < maxLength; i++) {
|
||||
const part1 = parts1[i] ?? 0;
|
||||
const part2 = parts2[i] ?? 0;
|
||||
|
||||
if (part1 > part2) return 1;
|
||||
if (part1 < part2) return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Private: Parse the static releases array format
|
||||
// This is a static JSON file containing all releases
|
||||
private parseStaticReleasFile (json: any, currentVersion: string): { ok: boolean; available: boolean; url?: string; name?: string; notes?: string; pub_date?: string } {
|
||||
if (!Array.isArray(json.releases) || !json.currentRelease || typeof json.currentRelease !== 'string') {
|
||||
this.emitError(new Error('Invalid releases format. Expected \'releases\' array and \'currentRelease\' string.'));
|
||||
return { ok: false, available: false };
|
||||
}
|
||||
|
||||
// Use currentRelease property to determine if update is available
|
||||
const currentReleaseVersion = json.currentRelease;
|
||||
|
||||
// Compare current version with currentRelease
|
||||
const versionComparison = this.compareVersions(currentReleaseVersion, currentVersion);
|
||||
|
||||
// If versions match, we're up to date
|
||||
if (versionComparison === 0) {
|
||||
return { ok: true, available: false };
|
||||
}
|
||||
|
||||
// If currentRelease is older than current version, check allowAnyVersion
|
||||
if (versionComparison < 0) {
|
||||
// If allowAnyVersion is true, allow downgrades
|
||||
if (this.allowAnyVersion) {
|
||||
// Continue to find the release entry for downgrade
|
||||
} else {
|
||||
return { ok: true, available: false };
|
||||
}
|
||||
}
|
||||
|
||||
// currentRelease is newer, find the release entry
|
||||
const releaseEntry = json.releases.find((r: any) => r.version === currentReleaseVersion);
|
||||
|
||||
if (!releaseEntry || !releaseEntry.updateTo) {
|
||||
this.emitError(new Error(`Release entry for version '${currentReleaseVersion}' not found or missing 'updateTo' property.`));
|
||||
return { ok: false, available: false };
|
||||
}
|
||||
|
||||
const updateTo = releaseEntry.updateTo;
|
||||
|
||||
if (!updateTo.url) {
|
||||
this.emitError(new Error(`Invalid release entry. 'updateTo.url' is missing for version ${currentReleaseVersion}.`));
|
||||
return { ok: false, available: false };
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
available: true,
|
||||
url: updateTo.url,
|
||||
name: updateTo.name,
|
||||
notes: updateTo.notes,
|
||||
pub_date: updateTo.pub_date
|
||||
};
|
||||
}
|
||||
|
||||
private parseDynamicReleasFile (json: any): { ok: boolean; available: boolean; url?: string; name?: string; notes?: string; pub_date?: string } {
|
||||
if (!json.url) {
|
||||
this.emitError(new Error('Invalid releases format. Expected \'url\' string property.'));
|
||||
return { ok: false, available: false };
|
||||
}
|
||||
return { ok: true, available: true, url: json.url, name: json.name, notes: json.notes, pub_date: json.pub_date };
|
||||
}
|
||||
|
||||
private async fetchSquirrelJson (url: string) {
|
||||
const headers: Record<string, string> = {
|
||||
...this.updateHeaders,
|
||||
Accept: 'application/json' // Always set Accept header, overriding any user-provided Accept
|
||||
};
|
||||
const response = await net.fetch(url, {
|
||||
headers
|
||||
});
|
||||
|
||||
if (response.status === 204) {
|
||||
return { ok: true, available: false };
|
||||
} else if (response.status === 200) {
|
||||
const updateJson = await response.json();
|
||||
|
||||
// Check if this is the static releases array format
|
||||
if (Array.isArray(updateJson.releases)) {
|
||||
// Get current package version
|
||||
const packageInfo = msixUpdate.getPackageInfo();
|
||||
const currentVersion = packageInfo.version;
|
||||
|
||||
if (!currentVersion) {
|
||||
this.emitError(new Error('Cannot determine current package version.'));
|
||||
return { ok: false, available: false };
|
||||
}
|
||||
|
||||
return this.parseStaticReleasFile(updateJson, currentVersion);
|
||||
} else {
|
||||
// Dynamic format: server returns JSON with update info for current version
|
||||
return this.parseDynamicReleasFile(updateJson);
|
||||
}
|
||||
} else {
|
||||
this.emitError(new Error(`Unexpected response status: ${response.status}`));
|
||||
return { ok: false, available: false };
|
||||
}
|
||||
}
|
||||
|
||||
private async getUpdateInfo (url: string): Promise<UpdateInfo> {
|
||||
if (url && await this.isDirectMsixUrl(url)) {
|
||||
return { ok: true, available: true, updateUrl: url, releaseDate: new Date() };
|
||||
} else {
|
||||
const updateJson = await this.fetchSquirrelJson(url);
|
||||
if (!updateJson.ok) {
|
||||
return { ok: false };
|
||||
} else if (updateJson.ok && !updateJson.available) {
|
||||
return { ok: true, available: false };
|
||||
} else {
|
||||
// updateJson.ok && updateJson.available must be true here
|
||||
// Parse the publication date if present (ISO 8601 format)
|
||||
let releaseDate: Date | null = null;
|
||||
if (updateJson.pub_date) {
|
||||
releaseDate = new Date(updateJson.pub_date);
|
||||
}
|
||||
|
||||
const updateUrl = updateJson.url ?? '';
|
||||
const releaseNotes = updateJson.notes ?? '';
|
||||
const releaseName = updateJson.name ?? '';
|
||||
releaseDate = releaseDate ?? new Date();
|
||||
|
||||
if (!await this.isDirectMsixUrl(updateUrl, true)) {
|
||||
return { ok: false };
|
||||
} else {
|
||||
return {
|
||||
ok: true,
|
||||
available: true,
|
||||
updateUrl,
|
||||
releaseNotes,
|
||||
releaseName,
|
||||
releaseDate
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getFeedURL () {
|
||||
return this.updateURL ?? '';
|
||||
}
|
||||
|
||||
setFeedURL (options: { url: string; headers?: Record<string, string>; allowAnyVersion?: boolean } | string) {
|
||||
let updateURL: string;
|
||||
let headers: Record<string, string> | undefined;
|
||||
let allowAnyVersion: boolean | undefined;
|
||||
if (typeof options === 'object') {
|
||||
if (typeof options.url === 'string') {
|
||||
updateURL = options.url;
|
||||
headers = options.headers;
|
||||
allowAnyVersion = options.allowAnyVersion;
|
||||
} else {
|
||||
throw new TypeError('Expected options object to contain a \'url\' string property in setFeedUrl call');
|
||||
}
|
||||
} else if (typeof options === 'string') {
|
||||
updateURL = options;
|
||||
} else {
|
||||
throw new TypeError('Expected an options object with a \'url\' property to be provided');
|
||||
}
|
||||
this.updateURL = updateURL;
|
||||
this.updateHeaders = headers ?? null;
|
||||
this.allowAnyVersion = allowAnyVersion ?? false;
|
||||
}
|
||||
|
||||
getPackageInfo (): MSIXPackageInfo {
|
||||
return msixUpdate.getPackageInfo() as MSIXPackageInfo;
|
||||
}
|
||||
|
||||
async checkForUpdates () {
|
||||
const url = this.updateURL;
|
||||
if (!url) {
|
||||
return this.emitError(new Error('Update URL is not set'));
|
||||
}
|
||||
|
||||
// Check if running in MSIX package
|
||||
const packageInfo = msixUpdate.getPackageInfo();
|
||||
if (!packageInfo.familyName) {
|
||||
return this.emitError(new Error('MSIX updates are not supported'));
|
||||
}
|
||||
|
||||
// If appInstallerUri is set, Windows App Installer manages updates automatically
|
||||
// Prevent updates here to avoid conflicts
|
||||
if (packageInfo.appInstallerUri) {
|
||||
return this.emitError(new Error('Auto-updates are managed by Windows App Installer. Updates are not allowed when installed via Application Manifest.'));
|
||||
}
|
||||
|
||||
this.emit('checking-for-update');
|
||||
try {
|
||||
const msixUrlInfo = await this.getUpdateInfo(url);
|
||||
if (!msixUrlInfo.ok) {
|
||||
return this.emitError(new Error('Invalid update or MSIX URL. See previous errors.'));
|
||||
}
|
||||
|
||||
if (!msixUrlInfo.available) {
|
||||
this.emit('update-not-available');
|
||||
} else {
|
||||
this.updateAvailable = true;
|
||||
this.emit('update-available');
|
||||
await msixUpdate.updateMsix(msixUrlInfo.updateUrl, {
|
||||
deferRegistration: true,
|
||||
developerMode: false,
|
||||
forceShutdown: false,
|
||||
forceTargetShutdown: false,
|
||||
forceUpdateFromAnyVersion: this.allowAnyVersion
|
||||
} as UpdateMsixOptions);
|
||||
|
||||
this.emit('update-downloaded', {}, msixUrlInfo.releaseNotes, msixUrlInfo.releaseName, msixUrlInfo.releaseDate, msixUrlInfo.updateUrl, () => {
|
||||
this.quitAndInstall();
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
this.emitError(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
async quitAndInstall () {
|
||||
if (!this.updateAvailable) {
|
||||
this.emitError(new Error('No update available, can\'t quit and install'));
|
||||
app.quit();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get package info to get family name
|
||||
const packageInfo = msixUpdate.getPackageInfo();
|
||||
if (!packageInfo.familyName) {
|
||||
return this.emitError(new Error('MSIX updates are not supported'));
|
||||
}
|
||||
|
||||
msixUpdate.registerRestartOnUpdate('');
|
||||
this.emit('before-quit-for-update');
|
||||
// force shutdown of the application and register the package to be installed on restart
|
||||
await msixUpdate.registerPackage(packageInfo.familyName, {
|
||||
forceShutdown: true
|
||||
} as RegisterPackageOptions);
|
||||
} catch (error) {
|
||||
this.emitError(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
// Private: Emit both error object and message, this is to keep compatibility
|
||||
// with Old APIs.
|
||||
emitError (error: Error) {
|
||||
this.emit('error', error, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
export default new AutoUpdater();
|
||||
@@ -20,6 +20,11 @@ class AutoUpdater extends EventEmitter implements Electron.AutoUpdater {
|
||||
return this.updateURL ?? '';
|
||||
}
|
||||
|
||||
getPackageInfo () {
|
||||
// Squirrel-based Windows apps don't have MSIX package information
|
||||
return undefined;
|
||||
}
|
||||
|
||||
setFeedURL (options: { url: string } | string) {
|
||||
let updateURL: string;
|
||||
if (typeof options === 'object') {
|
||||
|
||||
4
lib/browser/api/auto-updater/msix-update-win.ts
Normal file
4
lib/browser/api/auto-updater/msix-update-win.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
const { updateMsix, registerPackage, registerRestartOnUpdate, getPackageInfo } =
|
||||
process._linkedBinding('electron_browser_msix_updater');
|
||||
|
||||
export { updateMsix, registerPackage, registerRestartOnUpdate, getPackageInfo };
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"@electron/docs-parser": "^2.0.0",
|
||||
"@electron/fiddle-core": "^1.3.4",
|
||||
"@electron/github-app-auth": "^3.2.0",
|
||||
"@electron/lint-roller": "^3.1.2",
|
||||
"@electron/lint-roller": "^3.2.0",
|
||||
"@electron/typescript-definitions": "^9.1.2",
|
||||
"@octokit/rest": "^20.1.2",
|
||||
"@primer/octicons": "^10.0.0",
|
||||
@@ -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": {
|
||||
|
||||
@@ -147,3 +147,7 @@ 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
|
||||
loaf_add_feature_to_enable_sourceurl_for_all_protocols.patch
|
||||
|
||||
132
patches/chromium/cherry-pick-e045399a1ecb.patch
Normal file
132
patches/chromium/cherry-pick-e045399a1ecb.patch
Normal 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>
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -0,0 +1,52 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Niklas Wenzel <dev@nikwen.de>
|
||||
Date: Wed, 4 Feb 2026 06:02:40 -0800
|
||||
Subject: LoAF: Add feature to enable sourceURL for all protocols
|
||||
|
||||
Backports https://crrev.com/c/7510894 (minus the test changes).
|
||||
|
||||
Can be removed when that CL is included via a Chromium roll.
|
||||
|
||||
Change-Id: Id5e58a151b13cc0ac054f4ec237b038255d683fd
|
||||
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7510894
|
||||
Commit-Queue: Noam Rosenthal <nrosenthal@google.com>
|
||||
Reviewed-by: Dave Tapuska <dtapuska@chromium.org>
|
||||
Reviewed-by: Noam Rosenthal <nrosenthal@google.com>
|
||||
Cr-Commit-Position: refs/heads/main@{#1579397}
|
||||
|
||||
diff --git a/third_party/blink/renderer/core/frame/animation_frame_timing_monitor.cc b/third_party/blink/renderer/core/frame/animation_frame_timing_monitor.cc
|
||||
index 6e118aeed3b13e4d36064f2e114aba9edf4bb6dc..5e0304eda6548e7065527e03c0635bd1387138bd 100644
|
||||
--- a/third_party/blink/renderer/core/frame/animation_frame_timing_monitor.cc
|
||||
+++ b/third_party/blink/renderer/core/frame/animation_frame_timing_monitor.cc
|
||||
@@ -519,8 +519,15 @@ void AnimationFrameTimingMonitor::Trace(Visitor* visitor) const {
|
||||
visitor->Trace(frame_handling_input_);
|
||||
}
|
||||
|
||||
+BASE_FEATURE(kAlwaysLogLOAFURL, base::FEATURE_DISABLED_BY_DEFAULT);
|
||||
+
|
||||
namespace {
|
||||
+
|
||||
bool ShouldAllowScriptURL(const String& url) {
|
||||
+ if (base::FeatureList::IsEnabled(kAlwaysLogLOAFURL)) {
|
||||
+ return true;
|
||||
+ }
|
||||
+
|
||||
KURL kurl(url);
|
||||
return kurl.ProtocolIsData() || kurl.ProtocolIsInHTTPFamily() ||
|
||||
kurl.ProtocolIs("blob") || kurl.IsEmpty();
|
||||
diff --git a/third_party/blink/renderer/core/frame/animation_frame_timing_monitor.h b/third_party/blink/renderer/core/frame/animation_frame_timing_monitor.h
|
||||
index f2fe39be2db525d89fcd9787c2ae9285babab26d..c395cf39d404f6c4f6f6e23c9fb8dfe92151a7d2 100644
|
||||
--- a/third_party/blink/renderer/core/frame/animation_frame_timing_monitor.h
|
||||
+++ b/third_party/blink/renderer/core/frame/animation_frame_timing_monitor.h
|
||||
@@ -22,6 +22,11 @@ class TimeTicks;
|
||||
|
||||
namespace blink {
|
||||
|
||||
+// When enabled, long-animation-frame events will always include the sourceURL,
|
||||
+// regardless of protocol. This is useful during development when using `file:`
|
||||
+// URLs or custom protocols defined by embedders.
|
||||
+CORE_EXPORT BASE_DECLARE_FEATURE(kAlwaysLogLOAFURL);
|
||||
+
|
||||
class LocalFrame;
|
||||
|
||||
// Monitors long-animation-frame timing (LoAF).
|
||||
@@ -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
|
||||
@@ -385,7 +396,7 @@ index 71158ca9a7101911bb76f0c1b5300b0ff0e326b3..1441b9d4f9560c8b26d4beffe31449ed
|
||||
// The NSWindow used by BridgedNativeWidget. Provides hooks into AppKit that
|
||||
// can only be accomplished by overriding methods.
|
||||
diff --git a/components/remote_cocoa/app_shim/native_widget_mac_nswindow.mm b/components/remote_cocoa/app_shim/native_widget_mac_nswindow.mm
|
||||
index 433f12928857e288b6b0d4f4dd3d1f29da08cf6c..bb95aabec2e0d5c7b3a5d315c9a3f8cb3a45c8d2 100644
|
||||
index 433f12928857e288b6b0d4f4dd3d1f29da08cf6c..98bd545ad806c0f3eece970560c769a4b119a486 100644
|
||||
--- a/components/remote_cocoa/app_shim/native_widget_mac_nswindow.mm
|
||||
+++ b/components/remote_cocoa/app_shim/native_widget_mac_nswindow.mm
|
||||
@@ -21,6 +21,7 @@
|
||||
@@ -415,7 +426,29 @@ index 433f12928857e288b6b0d4f4dd3d1f29da08cf6c..bb95aabec2e0d5c7b3a5d315c9a3f8cb
|
||||
- (BOOL)hasKeyAppearance;
|
||||
- (long long)_resizeDirectionForMouseLocation:(CGPoint)location;
|
||||
- (BOOL)_isConsideredOpenForPersistentState;
|
||||
@@ -165,6 +170,8 @@ - (void)cr_mouseDownOnFrameView:(NSEvent*)event {
|
||||
@@ -158,13 +163,30 @@ - (void)cr_mouseDownOnFrameView:(NSEvent*)event;
|
||||
@implementation NSView (CRFrameViewAdditions)
|
||||
// If a mouseDown: falls through to the frame view, turn it into a window drag.
|
||||
- (void)cr_mouseDownOnFrameView:(NSEvent*)event {
|
||||
+#if !IS_MAS_BUILD()
|
||||
if ([self.window _resizeDirectionForMouseLocation:event.locationInWindow] !=
|
||||
-1)
|
||||
return;
|
||||
+#else
|
||||
+ // For MAS builds, approximate the resize direction check.
|
||||
+ if (self.window.styleMask & NSWindowStyleMaskResizable) {
|
||||
+ constexpr CGFloat kResizeThreshold = 5.0;
|
||||
+ NSPoint location = event.locationInWindow;
|
||||
+ NSRect frame = self.window.frame;
|
||||
+ CGFloat width = NSWidth(frame);
|
||||
+ CGFloat height = NSHeight(frame);
|
||||
+ if (location.x < kResizeThreshold || location.x > width - kResizeThreshold ||
|
||||
+ location.y < kResizeThreshold || location.y > height - kResizeThreshold) {
|
||||
+ return;
|
||||
+ }
|
||||
+ }
|
||||
+#endif
|
||||
[self.window performWindowDragWithEvent:event];
|
||||
}
|
||||
@end
|
||||
|
||||
@@ -424,7 +457,7 @@ index 433f12928857e288b6b0d4f4dd3d1f29da08cf6c..bb95aabec2e0d5c7b3a5d315c9a3f8cb
|
||||
@implementation NativeWidgetMacNSWindowTitledFrame
|
||||
- (void)mouseDown:(NSEvent*)event {
|
||||
if (self.window.isMovable)
|
||||
@@ -192,6 +199,8 @@ - (BOOL)usesCustomDrawing {
|
||||
@@ -192,6 +214,8 @@ - (BOOL)usesCustomDrawing {
|
||||
}
|
||||
@end
|
||||
|
||||
@@ -433,7 +466,7 @@ index 433f12928857e288b6b0d4f4dd3d1f29da08cf6c..bb95aabec2e0d5c7b3a5d315c9a3f8cb
|
||||
@implementation NativeWidgetMacNSWindow {
|
||||
@private
|
||||
CommandDispatcher* __strong _commandDispatcher;
|
||||
@@ -389,6 +398,8 @@ - (NSAccessibilityRole)accessibilityRole {
|
||||
@@ -389,6 +413,8 @@ - (NSAccessibilityRole)accessibilityRole {
|
||||
|
||||
// NSWindow overrides.
|
||||
|
||||
@@ -442,7 +475,7 @@ index 433f12928857e288b6b0d4f4dd3d1f29da08cf6c..bb95aabec2e0d5c7b3a5d315c9a3f8cb
|
||||
+ (Class)frameViewClassForStyleMask:(NSWindowStyleMask)windowStyle {
|
||||
if (windowStyle & NSWindowStyleMaskTitled) {
|
||||
if (Class customFrame = [NativeWidgetMacNSWindowTitledFrame class])
|
||||
@@ -400,6 +411,8 @@ + (Class)frameViewClassForStyleMask:(NSWindowStyleMask)windowStyle {
|
||||
@@ -400,6 +426,8 @@ + (Class)frameViewClassForStyleMask:(NSWindowStyleMask)windowStyle {
|
||||
return [super frameViewClassForStyleMask:windowStyle];
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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
1
patches/skia/.patches
Normal file
@@ -0,0 +1 @@
|
||||
graphite_add_insertstatus_koutoforderrecording.patch
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
32
script/copy-pipeline-segment-publish.js
Normal file
32
script/copy-pipeline-segment-publish.js
Normal 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)
|
||||
);
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -43,13 +43,10 @@
|
||||
#include "shell/browser/electron_gpu_client.h"
|
||||
#include "shell/browser/feature_list.h"
|
||||
#include "shell/browser/relauncher.h"
|
||||
#include "shell/common/application_info.h"
|
||||
#include "shell/common/electron_paths.h"
|
||||
#include "shell/common/logging.h"
|
||||
#include "shell/common/options_switches.h"
|
||||
#include "shell/common/platform_util.h"
|
||||
#include "shell/common/process_util.h"
|
||||
#include "shell/common/thread_restrictions.h"
|
||||
#include "shell/renderer/electron_renderer_client.h"
|
||||
#include "shell/renderer/electron_sandboxed_renderer_client.h"
|
||||
#include "shell/utility/electron_content_utility_client.h"
|
||||
@@ -121,100 +118,6 @@ void InvalidParameterHandler(const wchar_t*,
|
||||
}
|
||||
#endif
|
||||
|
||||
// TODO(nornagon): move path provider overriding to its own file in
|
||||
// shell/common
|
||||
bool ElectronPathProvider(int key, base::FilePath* result) {
|
||||
bool create_dir = false;
|
||||
base::FilePath cur;
|
||||
switch (key) {
|
||||
case chrome::DIR_USER_DATA:
|
||||
if (!base::PathService::Get(DIR_APP_DATA, &cur))
|
||||
return false;
|
||||
cur = cur.Append(base::FilePath::FromUTF8Unsafe(
|
||||
GetPossiblyOverriddenApplicationName()));
|
||||
create_dir = true;
|
||||
break;
|
||||
case DIR_CRASH_DUMPS:
|
||||
if (!base::PathService::Get(chrome::DIR_USER_DATA, &cur))
|
||||
return false;
|
||||
cur = cur.Append(FILE_PATH_LITERAL("Crashpad"));
|
||||
create_dir = true;
|
||||
break;
|
||||
case chrome::DIR_APP_DICTIONARIES:
|
||||
// TODO(nornagon): can we just default to using Chrome's logic here?
|
||||
if (!base::PathService::Get(DIR_SESSION_DATA, &cur))
|
||||
return false;
|
||||
cur = cur.Append(base::FilePath::FromUTF8Unsafe("Dictionaries"));
|
||||
create_dir = true;
|
||||
break;
|
||||
case DIR_SESSION_DATA:
|
||||
// By default and for backward, equivalent to DIR_USER_DATA.
|
||||
return base::PathService::Get(chrome::DIR_USER_DATA, result);
|
||||
case DIR_USER_CACHE: {
|
||||
#if BUILDFLAG(IS_POSIX)
|
||||
int parent_key = base::DIR_CACHE;
|
||||
#else
|
||||
// On Windows, there's no OS-level centralized location for caches, so
|
||||
// store the cache in the app data directory.
|
||||
int parent_key = base::DIR_ROAMING_APP_DATA;
|
||||
#endif
|
||||
if (!base::PathService::Get(parent_key, &cur))
|
||||
return false;
|
||||
cur = cur.Append(base::FilePath::FromUTF8Unsafe(
|
||||
GetPossiblyOverriddenApplicationName()));
|
||||
create_dir = true;
|
||||
break;
|
||||
}
|
||||
#if BUILDFLAG(IS_LINUX)
|
||||
case DIR_APP_DATA: {
|
||||
auto env = base::Environment::Create();
|
||||
cur = base::nix::GetXDGDirectory(
|
||||
env.get(), base::nix::kXdgConfigHomeEnvVar, base::nix::kDotConfigDir);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
case DIR_RECENT:
|
||||
if (!platform_util::GetFolderPath(DIR_RECENT, &cur))
|
||||
return false;
|
||||
create_dir = true;
|
||||
break;
|
||||
#endif
|
||||
case DIR_APP_LOGS:
|
||||
#if BUILDFLAG(IS_MAC)
|
||||
if (!base::PathService::Get(base::DIR_HOME, &cur))
|
||||
return false;
|
||||
cur = cur.Append(FILE_PATH_LITERAL("Library"));
|
||||
cur = cur.Append(FILE_PATH_LITERAL("Logs"));
|
||||
cur = cur.Append(base::FilePath::FromUTF8Unsafe(
|
||||
GetPossiblyOverriddenApplicationName()));
|
||||
#else
|
||||
if (!base::PathService::Get(chrome::DIR_USER_DATA, &cur))
|
||||
return false;
|
||||
cur = cur.Append(base::FilePath::FromUTF8Unsafe("logs"));
|
||||
#endif
|
||||
create_dir = true;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO(bauerb): http://crbug.com/259796
|
||||
ScopedAllowBlockingForElectron allow_blocking;
|
||||
if (create_dir && !base::PathExists(cur) && !base::CreateDirectory(cur)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*result = cur;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void RegisterPathProvider() {
|
||||
base::PathService::RegisterProvider(ElectronPathProvider, PATH_START,
|
||||
PATH_END);
|
||||
}
|
||||
|
||||
void ValidateV8Snapshot(v8::StartupData* data) {
|
||||
if (data->data &&
|
||||
electron::fuses::IsEmbeddedAsarIntegrityValidationEnabled()) {
|
||||
|
||||
909
shell/browser/api/electron_api_msix_updater.cc
Normal file
909
shell/browser/api/electron_api_msix_updater.cc
Normal file
@@ -0,0 +1,909 @@
|
||||
// Copyright (c) 2025 GitHub, Inc.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "shell/browser/api/electron_api_msix_updater.h"
|
||||
|
||||
#include <sstream>
|
||||
#include <string_view>
|
||||
#include "base/environment.h"
|
||||
#include "base/functional/bind.h"
|
||||
#include "base/logging.h"
|
||||
#include "base/strings/utf_string_conversions.h"
|
||||
#include "base/task/single_thread_task_runner.h"
|
||||
#include "content/public/browser/browser_task_traits.h"
|
||||
#include "content/public/browser/browser_thread.h"
|
||||
#include "shell/browser/javascript_environment.h"
|
||||
#include "shell/common/gin_helper/dictionary.h"
|
||||
#include "shell/common/gin_helper/error_thrower.h"
|
||||
#include "shell/common/gin_helper/promise.h"
|
||||
#include "shell/common/node_includes.h"
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
#include <appmodel.h>
|
||||
#include <roapi.h>
|
||||
#include <windows.applicationmodel.h>
|
||||
#include <windows.foundation.collections.h>
|
||||
#include <windows.foundation.h>
|
||||
#include <windows.foundation.metadata.h>
|
||||
#include <windows.h>
|
||||
#include <windows.management.deployment.h>
|
||||
#include <wrl.h>
|
||||
|
||||
#include "base/win/core_winrt_util.h"
|
||||
#include "base/win/scoped_hstring.h"
|
||||
#endif
|
||||
|
||||
namespace electron {
|
||||
|
||||
const bool debug_msix_updater =
|
||||
base::Environment::Create()->HasVar("ELECTRON_DEBUG_MSIX_UPDATER");
|
||||
|
||||
} // namespace electron
|
||||
|
||||
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)
|
||||
LOG(INFO) << std::string(log_msg);
|
||||
}
|
||||
|
||||
// Check if the process has a package identity
|
||||
bool HasPackageIdentity() {
|
||||
UINT32 length = 0;
|
||||
LONG rc = GetCurrentPackageFullName(&length, NULL);
|
||||
return rc != APPMODEL_ERROR_NO_PACKAGE;
|
||||
}
|
||||
|
||||
// POD struct to hold MSIX update options
|
||||
struct UpdateMsixOptions {
|
||||
bool defer_registration = false;
|
||||
bool developer_mode = false;
|
||||
bool force_shutdown = false;
|
||||
bool force_target_shutdown = false;
|
||||
bool force_update_from_any_version = false;
|
||||
};
|
||||
|
||||
// POD struct to hold package registration options
|
||||
struct RegisterPackageOptions {
|
||||
bool force_shutdown = false;
|
||||
bool force_target_shutdown = false;
|
||||
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");
|
||||
std::string error;
|
||||
|
||||
// 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;
|
||||
oss << "Calling AddPackageByUriAsync... URI: " << package_uri;
|
||||
DebugLog(oss.str());
|
||||
}
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "Update options - deferRegistration: " << opts.defer_registration
|
||||
<< ", developerMode: " << opts.developer_mode
|
||||
<< ", forceShutdown: " << opts.force_shutdown
|
||||
<< ", forceTargetShutdown: " << opts.force_target_shutdown
|
||||
<< ", forceUpdateFromAnyVersion: "
|
||||
<< opts.force_update_from_any_version;
|
||||
DebugLog(oss.str());
|
||||
}
|
||||
|
||||
// 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.";
|
||||
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;
|
||||
}
|
||||
|
||||
// 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
|
||||
void DoRegisterPackage(const std::string& family_name,
|
||||
RegisterPackageOptions opts,
|
||||
scoped_refptr<base::SingleThreadTaskRunner> reply_runner,
|
||||
gin_helper::Promise<void> promise) {
|
||||
DebugLog("DoRegisterPackage: Starting");
|
||||
std::string error;
|
||||
|
||||
// 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) {
|
||||
deployment_options = static_cast<DeploymentOptions>(
|
||||
deployment_options | DeploymentOptions_ForceApplicationShutdown);
|
||||
}
|
||||
if (opts.force_target_shutdown) {
|
||||
deployment_options = static_cast<DeploymentOptions>(
|
||||
deployment_options | DeploymentOptions_ForceTargetApplicationShutdown);
|
||||
}
|
||||
if (opts.force_update_from_any_version) {
|
||||
deployment_options = static_cast<DeploymentOptions>(
|
||||
deployment_options | DeploymentOptions_ForceUpdateFromAnyVersion);
|
||||
}
|
||||
|
||||
// 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;
|
||||
oss << "Calling RegisterPackageByFamilyNameAsync... FamilyName: "
|
||||
<< family_name;
|
||||
DebugLog(oss.str());
|
||||
}
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "Registration options - forceShutdown: " << opts.force_shutdown
|
||||
<< ", forceTargetShutdown: " << opts.force_target_shutdown
|
||||
<< ", forceUpdateFromAnyVersion: "
|
||||
<< opts.force_update_from_any_version;
|
||||
DebugLog(oss.str());
|
||||
}
|
||||
|
||||
// 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 (FAILED(hr) || !async_op) {
|
||||
error =
|
||||
"Deployment is NULL. See "
|
||||
"http://go.microsoft.com/fwlink/?LinkId=235160 for diagnosing.";
|
||||
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;
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// Update MSIX package
|
||||
v8::Local<v8::Promise> UpdateMsix(const std::string& package_uri,
|
||||
gin_helper::Dictionary options) {
|
||||
v8::Isolate* isolate = electron::JavascriptEnvironment::GetIsolate();
|
||||
gin_helper::Promise<void> promise(isolate);
|
||||
v8::Local<v8::Promise> handle = promise.GetHandle();
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
if (!HasPackageIdentity()) {
|
||||
DebugLog("UpdateMsix: The process has no package identity");
|
||||
promise.RejectWithErrorMessage("The process has no package identity.");
|
||||
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);
|
||||
options.Get("developerMode", &opts.developer_mode);
|
||||
options.Get("forceShutdown", &opts.force_shutdown);
|
||||
options.Get("forceTargetShutdown", &opts.force_target_shutdown);
|
||||
options.Get("forceUpdateFromAnyVersion", &opts.force_update_from_any_version);
|
||||
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "UpdateMsix called with URI: " << package_uri;
|
||||
DebugLog(oss.str());
|
||||
}
|
||||
|
||||
// Post to IO thread
|
||||
content::GetIOThreadTaskRunner({})->PostTask(
|
||||
FROM_HERE,
|
||||
base::BindOnce(&DoUpdateMsix, package_uri, opts,
|
||||
base::SingleThreadTaskRunner::GetCurrentDefault(),
|
||||
std::move(promise)));
|
||||
#else
|
||||
promise.RejectWithErrorMessage(
|
||||
"MSIX updates are only supported on Windows with identity.");
|
||||
#endif
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
// Register MSIX package
|
||||
v8::Local<v8::Promise> RegisterPackage(const std::string& family_name,
|
||||
gin_helper::Dictionary options) {
|
||||
v8::Isolate* isolate = electron::JavascriptEnvironment::GetIsolate();
|
||||
gin_helper::Promise<void> promise(isolate);
|
||||
v8::Local<v8::Promise> handle = promise.GetHandle();
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
if (!HasPackageIdentity()) {
|
||||
DebugLog("RegisterPackage: The process has no package identity");
|
||||
promise.RejectWithErrorMessage("The process has no package identity.");
|
||||
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);
|
||||
options.Get("forceTargetShutdown", &opts.force_target_shutdown);
|
||||
options.Get("forceUpdateFromAnyVersion", &opts.force_update_from_any_version);
|
||||
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "RegisterPackage called with family name: " << family_name;
|
||||
DebugLog(oss.str());
|
||||
}
|
||||
|
||||
// Post to IO thread with POD options (no V8 objects)
|
||||
content::GetIOThreadTaskRunner({})->PostTask(
|
||||
FROM_HERE,
|
||||
base::BindOnce(&DoRegisterPackage, family_name, opts,
|
||||
base::SingleThreadTaskRunner::GetCurrentDefault(),
|
||||
std::move(promise)));
|
||||
#else
|
||||
promise.RejectWithErrorMessage(
|
||||
"MSIX package registration is only supported on Windows.");
|
||||
#endif
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
// Register application restart
|
||||
// Only registers for update restarts (not crashes, hangs, or reboots)
|
||||
bool RegisterRestartOnUpdate(const std::string& command_line) {
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
if (!HasPackageIdentity()) {
|
||||
DebugLog("Cannot restart: no package identity");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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()) {
|
||||
command_line_wide = base::UTF8ToWide(command_line);
|
||||
command_line_ptr = command_line_wide.c_str();
|
||||
}
|
||||
|
||||
HRESULT hr = RegisterApplicationRestart(command_line_ptr, dwFlags);
|
||||
if (FAILED(hr)) {
|
||||
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());
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Get package information
|
||||
v8::Local<v8::Value> GetPackageInfo() {
|
||||
v8::Isolate* isolate = electron::JavascriptEnvironment::GetIsolate();
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
// Check if running in a package
|
||||
if (!HasPackageIdentity()) {
|
||||
DebugLog("GetPackageInfo: The process has no package identity");
|
||||
gin_helper::ErrorThrower thrower(isolate);
|
||||
thrower.ThrowTypeError("The process has no package identity.");
|
||||
return v8::Null(isolate);
|
||||
}
|
||||
|
||||
DebugLog("GetPackageInfo: Retrieving package information");
|
||||
|
||||
gin_helper::Dictionary result(isolate, v8::Object::New(isolate));
|
||||
|
||||
// Check API contract version (Windows 10 version 1703 or later)
|
||||
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 (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());
|
||||
}
|
||||
|
||||
// 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 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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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 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
|
||||
result.Set("familyName", "");
|
||||
result.Set("id", "");
|
||||
result.Set("developmentMode", false);
|
||||
result.Set("signatureKind", "none");
|
||||
result.Set("version", "");
|
||||
}
|
||||
|
||||
return result.GetHandle();
|
||||
#else
|
||||
// Non-Windows platforms
|
||||
gin_helper::Dictionary result(isolate, v8::Object::New(isolate));
|
||||
result.Set("familyName", "");
|
||||
result.Set("id", "");
|
||||
result.Set("developmentMode", false);
|
||||
result.Set("signatureKind", "none");
|
||||
result.Set("version", "");
|
||||
return result.GetHandle();
|
||||
#endif
|
||||
}
|
||||
|
||||
void Initialize(v8::Local<v8::Object> exports,
|
||||
v8::Local<v8::Value> unused,
|
||||
v8::Local<v8::Context> context,
|
||||
void* priv) {
|
||||
v8::Isolate* const isolate = electron::JavascriptEnvironment::GetIsolate();
|
||||
gin_helper::Dictionary dict(isolate, exports);
|
||||
|
||||
dict.SetMethod("updateMsix", base::BindRepeating(&UpdateMsix));
|
||||
dict.SetMethod("registerPackage", base::BindRepeating(&RegisterPackage));
|
||||
dict.SetMethod("registerRestartOnUpdate",
|
||||
base::BindRepeating(&RegisterRestartOnUpdate));
|
||||
dict.SetMethod("getPackageInfo",
|
||||
base::BindRepeating([]() { return GetPackageInfo(); }));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
NODE_LINKED_BINDING_CONTEXT_AWARE(electron_browser_msix_updater, Initialize)
|
||||
14
shell/browser/api/electron_api_msix_updater.h
Normal file
14
shell/browser/api/electron_api_msix_updater.h
Normal file
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2013 GitHub, Inc.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#ifndef ELECTRON_SHELL_BROWSER_API_ELECTRON_API_MSIX_UPDATER_H_
|
||||
#define ELECTRON_SHELL_BROWSER_API_ELECTRON_API_MSIX_UPDATER_H_
|
||||
|
||||
namespace electron {
|
||||
|
||||
extern const bool debug_msix_updater;
|
||||
|
||||
} // namespace electron
|
||||
|
||||
#endif // ELECTRON_SHELL_BROWSER_API_ELECTRON_API_MSIX_UPDATER_H_
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -83,8 +83,16 @@ void WebContentsView::ApplyBorderRadius() {
|
||||
|
||||
int WebContentsView::NonClientHitTest(const gfx::Point& point) {
|
||||
if (api_web_contents_) {
|
||||
// Convert the point to the contents view's coordinate space rather than
|
||||
// the InspectableWebContentsView's coordinate space, because the draggable
|
||||
// region is relative to the web content area. When DevTools is docked
|
||||
// (e.g. to the left), the contents view is offset within the parent,
|
||||
// so we need to account for that offset.
|
||||
auto* inspectable_view =
|
||||
api_web_contents_->inspectable_web_contents()->GetView();
|
||||
auto* contents_view = inspectable_view->GetContentsView();
|
||||
gfx::Point local_point(point);
|
||||
views::View::ConvertPointFromWidget(view(), &local_point);
|
||||
views::View::ConvertPointFromWidget(contents_view, &local_point);
|
||||
SkRegion* region = api_web_contents_->draggable_region();
|
||||
if (region && region->contains(local_point.x(), local_point.y()))
|
||||
return HTCAPTION;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,11 +56,6 @@ static NSDictionary* UNNotificationResponseToNSDictionary(
|
||||
}
|
||||
|
||||
- (void)applicationWillFinishLaunching:(NSNotification*)notify {
|
||||
// Don't add the "Enter Full Screen" menu item automatically.
|
||||
[[NSUserDefaults standardUserDefaults]
|
||||
setBool:NO
|
||||
forKey:@"NSFullScreenMenuItemEverywhere"];
|
||||
|
||||
[[[NSWorkspace sharedWorkspace] notificationCenter]
|
||||
addObserver:self
|
||||
selector:@selector(willPowerOff:)
|
||||
@@ -114,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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -258,7 +258,7 @@ NSArray* ConvertSharingItemToNS(const SharingItem& item) {
|
||||
|
||||
// Empties the source menu items to the destination.
|
||||
- (void)moveMenuItems:(NSMenu*)source to:(NSMenu*)destination {
|
||||
const NSInteger count = [source numberOfItems];
|
||||
const NSInteger count = source.numberOfItems;
|
||||
for (NSInteger index = 0; index < count; index++) {
|
||||
NSMenuItem* removedItem = [source itemAtIndex:0];
|
||||
[source removeItemAtIndex:0];
|
||||
@@ -269,25 +269,25 @@ NSArray* ConvertSharingItemToNS(const SharingItem& item) {
|
||||
// Replaces the item's submenu instance with the singleton recent documents
|
||||
// menu. Previously replaced menu items will be recovered.
|
||||
- (void)replaceSubmenuShowingRecentDocuments:(NSMenuItem*)item {
|
||||
NSMenu* recentDocumentsMenu = [recentDocumentsMenuItem_ submenu];
|
||||
NSMenu* recentDocumentsMenu = recentDocumentsMenuItem_.submenu;
|
||||
|
||||
// Remove menu items in recent documents back to swap menu
|
||||
[self moveMenuItems:recentDocumentsMenu to:recentDocumentsMenuSwap_];
|
||||
// Swap back the submenu
|
||||
[recentDocumentsMenuItem_ setSubmenu:recentDocumentsMenuSwap_];
|
||||
recentDocumentsMenuItem_.submenu = recentDocumentsMenuSwap_;
|
||||
|
||||
// Retain the item's submenu for a future recovery
|
||||
recentDocumentsMenuSwap_ = [item submenu];
|
||||
recentDocumentsMenuSwap_ = item.submenu;
|
||||
|
||||
// Repopulate with items from the submenu to be replaced
|
||||
[self moveMenuItems:recentDocumentsMenuSwap_ to:recentDocumentsMenu];
|
||||
// Update the submenu's title
|
||||
[recentDocumentsMenu setTitle:[recentDocumentsMenuSwap_ title]];
|
||||
recentDocumentsMenu.title = recentDocumentsMenuSwap_.title;
|
||||
// Replace submenu
|
||||
[item setSubmenu:recentDocumentsMenu];
|
||||
item.submenu = recentDocumentsMenu;
|
||||
|
||||
DCHECK_EQ([item action], @selector(submenuAction:));
|
||||
DCHECK_EQ([item target], recentDocumentsMenu);
|
||||
DCHECK_EQ(item.action, @selector(submenuAction:));
|
||||
DCHECK_EQ(item.target, recentDocumentsMenu);
|
||||
|
||||
// Remember the new menu item that carries the recent documents menu
|
||||
recentDocumentsMenuItem_ = item;
|
||||
@@ -303,7 +303,7 @@ NSArray* ConvertSharingItemToNS(const SharingItem& item) {
|
||||
NSArray* services = [NSSharingService sharingServicesForItems:items];
|
||||
for (NSSharingService* service in services)
|
||||
[menu addItem:[self menuItemForService:service withItems:items]];
|
||||
[menu setDelegate:self];
|
||||
menu.delegate = self;
|
||||
return menu;
|
||||
}
|
||||
|
||||
@@ -313,9 +313,9 @@ NSArray* ConvertSharingItemToNS(const SharingItem& item) {
|
||||
NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:service.menuItemTitle
|
||||
action:@selector(performShare:)
|
||||
keyEquivalent:@""];
|
||||
[item setTarget:self];
|
||||
[item setImage:service.image];
|
||||
[item setRepresentedObject:@{@"service" : service, @"items" : items}];
|
||||
item.target = self;
|
||||
item.image = service.image;
|
||||
item.representedObject = @{@"service" : service, @"items" : items};
|
||||
return item;
|
||||
}
|
||||
|
||||
@@ -398,7 +398,7 @@ NSArray* ConvertSharingItemToNS(const SharingItem& item) {
|
||||
submenu.delegate = self;
|
||||
|
||||
// Set submenu's role.
|
||||
if ((role == u"window" || role == u"windowmenu") && [submenu numberOfItems])
|
||||
if ((role == u"window" || role == u"windowmenu") && submenu.numberOfItems)
|
||||
[NSApp setWindowsMenu:submenu];
|
||||
else if (role == u"help")
|
||||
[NSApp setHelpMenu:submenu];
|
||||
@@ -463,6 +463,14 @@ NSArray* ConvertSharingItemToNS(const SharingItem& item) {
|
||||
return item;
|
||||
}
|
||||
|
||||
// Called by AppKit before displaying a menu and when a key equivalent is
|
||||
// pressed. This ensures menu item states (enabled, checked, hidden) are
|
||||
// refreshed from the model even when the menu is closed, which is necessary
|
||||
// since we set autoenablesItems = NO.
|
||||
- (void)menuNeedsUpdate:(NSMenu*)menu {
|
||||
[self refreshMenuTree:menu];
|
||||
}
|
||||
|
||||
- (void)applyStateToMenuItem:(NSMenuItem*)item {
|
||||
id represented = item.representedObject;
|
||||
if (!represented)
|
||||
@@ -484,14 +492,15 @@ NSArray* ConvertSharingItemToNS(const SharingItem& item) {
|
||||
if (index < 0 || index >= count)
|
||||
return;
|
||||
|
||||
item.hidden = !model->IsVisibleAt(index);
|
||||
// When the menu is closed, we need to allow shortcuts to be triggered even
|
||||
// if the menu item is disabled. So we only disable the menu item when the
|
||||
// menu is open. This matches behavior of |validateUserInterfaceItem|.
|
||||
item.enabled = model->IsEnabledAt(index);
|
||||
item.hidden = !model->IsVisibleAt(index);
|
||||
item.state = model->IsItemCheckedAt(index) ? NSControlStateValueOn
|
||||
: 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];
|
||||
@@ -557,7 +566,14 @@ NSArray* ConvertSharingItemToNS(const SharingItem& item) {
|
||||
|
||||
- (void)menuWillOpen:(NSMenu*)menu {
|
||||
isMenuOpen_ = YES;
|
||||
[self refreshMenuTree:menu];
|
||||
|
||||
// macOS automatically injects a duplicate "Toggle Full Screen" menu item
|
||||
// when we set menu.delegate on submenus. Remove hidden duplicates.
|
||||
for (NSMenuItem* item in menu.itemArray) {
|
||||
if (item.isHidden && item.action == @selector(toggleFullScreenMode:))
|
||||
[menu removeItem:item];
|
||||
}
|
||||
|
||||
if (model_)
|
||||
model_->MenuWillShow();
|
||||
}
|
||||
@@ -567,18 +583,18 @@ NSArray* ConvertSharingItemToNS(const SharingItem& item) {
|
||||
if (!isMenuOpen_)
|
||||
return;
|
||||
|
||||
bool has_close_cb = !popupCloseCallback.is_null();
|
||||
isMenuOpen_ = NO;
|
||||
|
||||
// There are two scenarios where we should emit menu-did-close:
|
||||
// 1. It's a popup and the top level menu is closed.
|
||||
// 2. It's an application menu, and the current menu's supermenu
|
||||
// is the top-level menu.
|
||||
bool has_close_cb = !popupCloseCallback.is_null();
|
||||
if (menu != menu_) {
|
||||
if (has_close_cb || menu.supermenu != menu_)
|
||||
return;
|
||||
}
|
||||
|
||||
isMenuOpen_ = NO;
|
||||
if (model_)
|
||||
model_->MenuWillClose();
|
||||
// Post async task so that itemSelected runs before the close callback
|
||||
|
||||
@@ -82,11 +82,13 @@ GdkPixbuf* GdkPixbufFromSkBitmap(const SkBitmap& bitmap) {
|
||||
constexpr GdkColorspace kColorspace = GDK_COLORSPACE_RGB;
|
||||
constexpr gboolean kHasAlpha = true;
|
||||
constexpr int kBitsPerSample = 8;
|
||||
return gdk_pixbuf_new_from_bytes(
|
||||
g_bytes_new(std::data(bytes), std::size(bytes)), kColorspace, kHasAlpha,
|
||||
kBitsPerSample, width, height,
|
||||
GBytes* gbytes = g_bytes_new(std::data(bytes), std::size(bytes));
|
||||
GdkPixbuf* pixbuf = gdk_pixbuf_new_from_bytes(
|
||||
gbytes, kColorspace, kHasAlpha, kBitsPerSample, width, height,
|
||||
gdk_pixbuf_calculate_rowstride(kColorspace, kHasAlpha, kBitsPerSample,
|
||||
width, height));
|
||||
g_bytes_unref(gbytes);
|
||||
return pixbuf;
|
||||
}
|
||||
|
||||
} // namespace gtk_util
|
||||
|
||||
@@ -62,9 +62,9 @@ class InspectableWebContentsView : public views::View {
|
||||
// views::View:
|
||||
void Layout(PassKey) override;
|
||||
|
||||
private:
|
||||
views::View* GetContentsView() const;
|
||||
|
||||
private:
|
||||
// Owns us.
|
||||
raw_ptr<InspectableWebContents> inspectable_web_contents_;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "components/input/native_web_keyboard_event.h"
|
||||
#include "shell/browser/native_window.h"
|
||||
#include "shell/browser/ui/views/menu_bar.h"
|
||||
#include "ui/events/keycodes/dom/keycode_converter.h"
|
||||
#include "ui/views/layout/box_layout.h"
|
||||
|
||||
namespace electron {
|
||||
@@ -21,9 +22,21 @@ bool IsAltKey(const input::NativeWebKeyboardEvent& event) {
|
||||
|
||||
bool IsAltModifier(const input::NativeWebKeyboardEvent& event) {
|
||||
using Mods = input::NativeWebKeyboardEvent::Modifiers;
|
||||
|
||||
// AltGraph (AltGr) should not be treated as a single Alt keypress for
|
||||
// menu-bar toggling.
|
||||
if (event.windows_key_code == ui::VKEY_ALTGR ||
|
||||
ui::KeycodeConverter::DomKeyToKeyString(event.dom_key) == "AltGraph") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (event.GetModifiers() & Mods::kKeyModifiers) == Mods::kAltKey;
|
||||
}
|
||||
|
||||
bool IsSingleAltKey(const input::NativeWebKeyboardEvent& event) {
|
||||
return IsAltKey(event) && IsAltModifier(event);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
RootView::RootView(NativeWindow* window)
|
||||
@@ -98,7 +111,7 @@ void RootView::HandleKeyEvent(const input::NativeWebKeyboardEvent& event) {
|
||||
return;
|
||||
|
||||
// Show accelerator when "Alt" is pressed.
|
||||
if (menu_bar_visible_ && IsAltKey(event))
|
||||
if (menu_bar_visible_ && IsSingleAltKey(event))
|
||||
menu_bar_->SetAcceleratorVisibility(
|
||||
event.GetType() == blink::WebInputEvent::Type::kRawKeyDown);
|
||||
|
||||
@@ -121,11 +134,11 @@ void RootView::HandleKeyEvent(const input::NativeWebKeyboardEvent& event) {
|
||||
|
||||
// Toggle the menu bar only when a single Alt is released.
|
||||
if (event.GetType() == blink::WebInputEvent::Type::kRawKeyDown &&
|
||||
IsAltKey(event)) {
|
||||
IsSingleAltKey(event)) {
|
||||
// When a single Alt is pressed:
|
||||
menu_bar_alt_pressed_ = true;
|
||||
} else if (event.GetType() == blink::WebInputEvent::Type::kKeyUp &&
|
||||
IsAltKey(event) && menu_bar_alt_pressed_) {
|
||||
IsSingleAltKey(event) && menu_bar_alt_pressed_) {
|
||||
// When a single Alt is released right after a Alt is pressed:
|
||||
menu_bar_alt_pressed_ = false;
|
||||
if (menu_bar_autohide_)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
117
shell/common/electron_paths.cc
Normal file
117
shell/common/electron_paths.cc
Normal file
@@ -0,0 +1,117 @@
|
||||
// Copyright (c) 2026 Microsoft GmbH.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "shell/common/electron_paths.h"
|
||||
|
||||
#include "base/environment.h"
|
||||
#include "base/files/file_util.h"
|
||||
#include "base/path_service.h"
|
||||
#include "chrome/common/chrome_paths.h"
|
||||
#include "shell/common/application_info.h"
|
||||
#include "shell/common/platform_util.h"
|
||||
#include "shell/common/thread_restrictions.h"
|
||||
|
||||
#if BUILDFLAG(IS_LINUX)
|
||||
#include "base/nix/xdg_util.h"
|
||||
#endif
|
||||
|
||||
namespace electron {
|
||||
|
||||
namespace {
|
||||
|
||||
bool ElectronPathProvider(int key, base::FilePath* result) {
|
||||
bool create_dir = false;
|
||||
base::FilePath cur;
|
||||
switch (key) {
|
||||
case chrome::DIR_USER_DATA:
|
||||
if (!base::PathService::Get(DIR_APP_DATA, &cur))
|
||||
return false;
|
||||
cur = cur.Append(base::FilePath::FromUTF8Unsafe(
|
||||
GetPossiblyOverriddenApplicationName()));
|
||||
create_dir = true;
|
||||
break;
|
||||
case DIR_CRASH_DUMPS:
|
||||
if (!base::PathService::Get(chrome::DIR_USER_DATA, &cur))
|
||||
return false;
|
||||
cur = cur.Append(FILE_PATH_LITERAL("Crashpad"));
|
||||
create_dir = true;
|
||||
break;
|
||||
case chrome::DIR_APP_DICTIONARIES:
|
||||
// TODO(nornagon): can we just default to using Chrome's logic here?
|
||||
if (!base::PathService::Get(DIR_SESSION_DATA, &cur))
|
||||
return false;
|
||||
cur = cur.Append(base::FilePath::FromUTF8Unsafe("Dictionaries"));
|
||||
create_dir = true;
|
||||
break;
|
||||
case DIR_SESSION_DATA:
|
||||
// By default and for backward, equivalent to DIR_USER_DATA.
|
||||
return base::PathService::Get(chrome::DIR_USER_DATA, result);
|
||||
case DIR_USER_CACHE: {
|
||||
#if BUILDFLAG(IS_POSIX)
|
||||
int parent_key = base::DIR_CACHE;
|
||||
#else
|
||||
// On Windows, there's no OS-level centralized location for caches, so
|
||||
// store the cache in the app data directory.
|
||||
int parent_key = base::DIR_ROAMING_APP_DATA;
|
||||
#endif
|
||||
if (!base::PathService::Get(parent_key, &cur))
|
||||
return false;
|
||||
cur = cur.Append(base::FilePath::FromUTF8Unsafe(
|
||||
GetPossiblyOverriddenApplicationName()));
|
||||
create_dir = true;
|
||||
break;
|
||||
}
|
||||
#if BUILDFLAG(IS_LINUX)
|
||||
case DIR_APP_DATA: {
|
||||
auto env = base::Environment::Create();
|
||||
cur = base::nix::GetXDGDirectory(
|
||||
env.get(), base::nix::kXdgConfigHomeEnvVar, base::nix::kDotConfigDir);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
case DIR_RECENT:
|
||||
if (!platform_util::GetFolderPath(DIR_RECENT, &cur))
|
||||
return false;
|
||||
create_dir = true;
|
||||
break;
|
||||
#endif
|
||||
case DIR_APP_LOGS:
|
||||
#if BUILDFLAG(IS_MAC)
|
||||
if (!base::PathService::Get(base::DIR_HOME, &cur))
|
||||
return false;
|
||||
cur = cur.Append(FILE_PATH_LITERAL("Library"));
|
||||
cur = cur.Append(FILE_PATH_LITERAL("Logs"));
|
||||
cur = cur.Append(base::FilePath::FromUTF8Unsafe(
|
||||
GetPossiblyOverriddenApplicationName()));
|
||||
#else
|
||||
if (!base::PathService::Get(chrome::DIR_USER_DATA, &cur))
|
||||
return false;
|
||||
cur = cur.Append(base::FilePath::FromUTF8Unsafe("logs"));
|
||||
#endif
|
||||
create_dir = true;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO(bauerb): http://crbug.com/259796
|
||||
ScopedAllowBlockingForElectron allow_blocking;
|
||||
if (create_dir && !base::PathExists(cur) && !base::CreateDirectory(cur)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*result = cur;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void RegisterPathProvider() {
|
||||
base::PathService::RegisterProvider(ElectronPathProvider, PATH_START,
|
||||
PATH_END);
|
||||
}
|
||||
|
||||
} // namespace electron
|
||||
@@ -6,6 +6,7 @@
|
||||
#define ELECTRON_SHELL_COMMON_ELECTRON_PATHS_H_
|
||||
|
||||
#include "base/base_paths.h"
|
||||
#include "base/files/file_path.h"
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
#include "base/base_paths_win.h"
|
||||
@@ -47,6 +48,9 @@ enum {
|
||||
|
||||
static_assert(PATH_START < PATH_END, "invalid PATH boundaries");
|
||||
|
||||
// Register the path provider with the base::PathService.
|
||||
void RegisterPathProvider();
|
||||
|
||||
} // namespace electron
|
||||
|
||||
#endif // ELECTRON_SHELL_COMMON_ELECTRON_PATHS_H_
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
V(electron_browser_in_app_purchase) \
|
||||
V(electron_browser_menu) \
|
||||
V(electron_browser_message_port) \
|
||||
V(electron_browser_msix_updater) \
|
||||
V(electron_browser_native_theme) \
|
||||
V(electron_browser_notification) \
|
||||
V(electron_browser_power_monitor) \
|
||||
|
||||
@@ -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',
|
||||
|
||||
336
spec/api-autoupdater-msix-spec.ts
Normal file
336
spec/api-autoupdater-msix-spec.ts
Normal file
@@ -0,0 +1,336 @@
|
||||
import { expect } from 'chai';
|
||||
import * as express from 'express';
|
||||
|
||||
import * as http from 'node:http';
|
||||
import { AddressInfo } from 'node:net';
|
||||
|
||||
import {
|
||||
getElectronExecutable,
|
||||
getMainJsFixturePath,
|
||||
getMsixFixturePath,
|
||||
getMsixPackageVersion,
|
||||
installMsixCertificate,
|
||||
installMsixPackage,
|
||||
registerExecutableWithIdentity,
|
||||
shouldRunMsixTests,
|
||||
spawn,
|
||||
uninstallMsixPackage,
|
||||
unregisterExecutableWithIdentity
|
||||
} from './lib/msix-helpers';
|
||||
import { ifdescribe } from './lib/spec-helpers';
|
||||
|
||||
const ELECTRON_MSIX_ALIAS = 'ElectronMSIX.exe';
|
||||
const MAIN_JS_PATH = getMainJsFixturePath();
|
||||
const MSIX_V1 = getMsixFixturePath('v1');
|
||||
const MSIX_V2 = getMsixFixturePath('v2');
|
||||
|
||||
// We can only test the MSIX updater on Windows
|
||||
ifdescribe(shouldRunMsixTests)('autoUpdater MSIX behavior', function () {
|
||||
this.timeout(120000);
|
||||
|
||||
before(async function () {
|
||||
await installMsixCertificate();
|
||||
|
||||
const electronExec = getElectronExecutable();
|
||||
await registerExecutableWithIdentity(electronExec);
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await unregisterExecutableWithIdentity();
|
||||
});
|
||||
|
||||
const launchApp = (executablePath: string, args: string[] = []) => {
|
||||
return spawn(executablePath, args);
|
||||
};
|
||||
|
||||
const logOnError = (what: any, fn: () => void) => {
|
||||
try {
|
||||
fn();
|
||||
} catch (err) {
|
||||
console.error(what);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
it('should launch Electron via MSIX alias', async () => {
|
||||
const launchResult = await launchApp(ELECTRON_MSIX_ALIAS, ['--version']);
|
||||
logOnError(launchResult, () => {
|
||||
expect(launchResult.code).to.equal(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should print package identity information', async () => {
|
||||
const launchResult = await launchApp(ELECTRON_MSIX_ALIAS, [MAIN_JS_PATH, '--printPackageId']);
|
||||
logOnError(launchResult, () => {
|
||||
expect(launchResult.code).to.equal(0);
|
||||
expect(launchResult.out).to.include('Family Name: Electron.Dev.MSIX_rdjwn13tdj8dy');
|
||||
expect(launchResult.out).to.include('Package ID: Electron.Dev.MSIX_1.0.0.0_x64__rdjwn13tdj8dy');
|
||||
expect(launchResult.out).to.include('Version: 1.0.0.0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with update server', () => {
|
||||
let port = 0;
|
||||
let server: express.Application = null as any;
|
||||
let httpServer: http.Server = null as any;
|
||||
let requests: express.Request[] = [];
|
||||
|
||||
beforeEach((done) => {
|
||||
requests = [];
|
||||
server = express();
|
||||
server.use((req, res, next) => {
|
||||
requests.push(req);
|
||||
next();
|
||||
});
|
||||
httpServer = server.listen(0, '127.0.0.1', () => {
|
||||
port = (httpServer.address() as AddressInfo).port;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (httpServer) {
|
||||
await new Promise<void>(resolve => {
|
||||
httpServer.close(() => {
|
||||
httpServer = null as any;
|
||||
server = null as any;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
await uninstallMsixPackage('com.electron.myapp');
|
||||
});
|
||||
|
||||
it('should not update when no update is available', async () => {
|
||||
server.get('/update-check', (req, res) => {
|
||||
res.status(204).send();
|
||||
});
|
||||
|
||||
const launchResult = await launchApp(ELECTRON_MSIX_ALIAS, [MAIN_JS_PATH, '--checkUpdate', `http://localhost:${port}/update-check`]);
|
||||
logOnError(launchResult, () => {
|
||||
expect(launchResult.code).to.equal(0);
|
||||
expect(requests.length).to.be.greaterThan(0);
|
||||
expect(launchResult.out).to.include('Checking for update...');
|
||||
expect(launchResult.out).to.include('Update not available');
|
||||
});
|
||||
});
|
||||
|
||||
it('should hit the update endpoint with custom headers when checkForUpdates is called', async () => {
|
||||
server.get('/update-check', (req, res) => {
|
||||
expect(req.headers['x-appversion']).to.equal('1.0.0');
|
||||
expect(req.headers.authorization).to.equal('Bearer test-token');
|
||||
res.status(204).send();
|
||||
});
|
||||
|
||||
const launchResult = await launchApp(ELECTRON_MSIX_ALIAS, [
|
||||
MAIN_JS_PATH,
|
||||
'--checkUpdate',
|
||||
`http://localhost:${port}/update-check`,
|
||||
'--useCustomHeaders'
|
||||
]);
|
||||
|
||||
logOnError(launchResult, () => {
|
||||
expect(launchResult.code).to.equal(0);
|
||||
expect(requests.length).to.be.greaterThan(0);
|
||||
expect(launchResult.out).to.include('Checking for update...');
|
||||
expect(launchResult.out).to.include('Update not available');
|
||||
});
|
||||
});
|
||||
|
||||
it('should update successfully with direct link to MSIX file', async () => {
|
||||
await installMsixPackage(MSIX_V1);
|
||||
const initialVersion = await getMsixPackageVersion('com.electron.myapp');
|
||||
expect(initialVersion).to.equal('1.0.0.0');
|
||||
|
||||
server.get('/update.msix', (req, res) => {
|
||||
res.download(MSIX_V2);
|
||||
});
|
||||
|
||||
const launchResult = await launchApp(ELECTRON_MSIX_ALIAS, [
|
||||
MAIN_JS_PATH,
|
||||
'--checkUpdate',
|
||||
`http://localhost:${port}/update.msix`
|
||||
]);
|
||||
|
||||
logOnError(launchResult, () => {
|
||||
expect(launchResult.code).to.equal(0);
|
||||
expect(requests.length).to.be.greaterThan(0);
|
||||
expect(launchResult.out).to.include('Checking for update...');
|
||||
expect(launchResult.out).to.include('Update available');
|
||||
expect(launchResult.out).to.include('Update downloaded');
|
||||
expect(launchResult.out).to.include('Release Name: N/A');
|
||||
expect(launchResult.out).to.include('Release Notes: N/A');
|
||||
expect(launchResult.out).to.include(`Update URL: http://localhost:${port}/update.msix`);
|
||||
});
|
||||
|
||||
const updatedVersion = await getMsixPackageVersion('com.electron.myapp');
|
||||
expect(updatedVersion).to.equal('2.0.0.0');
|
||||
});
|
||||
|
||||
it('should update successfully with JSON response', async () => {
|
||||
await installMsixPackage(MSIX_V1);
|
||||
const initialVersion = await getMsixPackageVersion('com.electron.myapp');
|
||||
expect(initialVersion).to.equal('1.0.0.0');
|
||||
|
||||
const fixedPubDate = '2011-11-11T11:11:11.000Z';
|
||||
const expectedDateStr = new Date(fixedPubDate).toDateString();
|
||||
|
||||
server.get('/update-check', (req, res) => {
|
||||
res.json({
|
||||
url: `http://localhost:${port}/update.msix`,
|
||||
name: '2.0.0',
|
||||
notes: 'Test release notes',
|
||||
pub_date: fixedPubDate
|
||||
});
|
||||
});
|
||||
|
||||
server.get('/update.msix', (req, res) => {
|
||||
res.download(MSIX_V2);
|
||||
});
|
||||
|
||||
const launchResult = await launchApp(ELECTRON_MSIX_ALIAS, [MAIN_JS_PATH, '--checkUpdate', `http://localhost:${port}/update-check`]);
|
||||
logOnError(launchResult, () => {
|
||||
expect(launchResult.code).to.equal(0);
|
||||
expect(requests.length).to.be.greaterThan(0);
|
||||
expect(launchResult.out).to.include('Checking for update...');
|
||||
expect(launchResult.out).to.include('Update available');
|
||||
expect(launchResult.out).to.include('Update downloaded');
|
||||
expect(launchResult.out).to.include('Release Name: 2.0.0');
|
||||
expect(launchResult.out).to.include('Release Notes: Test release notes');
|
||||
expect(launchResult.out).to.include(`Release Date: ${expectedDateStr}`);
|
||||
expect(launchResult.out).to.include(`Update URL: http://localhost:${port}/update.msix`);
|
||||
});
|
||||
|
||||
const updatedVersion = await getMsixPackageVersion('com.electron.myapp');
|
||||
expect(updatedVersion).to.equal('2.0.0.0');
|
||||
});
|
||||
|
||||
it('should update successfully with static JSON releases file', async () => {
|
||||
await installMsixPackage(MSIX_V1);
|
||||
const initialVersion = await getMsixPackageVersion('com.electron.myapp');
|
||||
expect(initialVersion).to.equal('1.0.0.0');
|
||||
|
||||
const fixedPubDate = '2011-11-11T11:11:11.000Z';
|
||||
const expectedDateStr = new Date(fixedPubDate).toDateString();
|
||||
|
||||
server.get('/update-check', (req, res) => {
|
||||
res.json({
|
||||
currentRelease: '2.0.0',
|
||||
releases: [
|
||||
{
|
||||
version: '1.0.0',
|
||||
updateTo: {
|
||||
version: '1.0.0',
|
||||
url: `http://localhost:${port}/update-v1.msix`,
|
||||
name: '1.0.0',
|
||||
notes: 'Initial release',
|
||||
pub_date: '2010-10-10T10:10:10.000Z'
|
||||
}
|
||||
},
|
||||
{
|
||||
version: '2.0.0',
|
||||
updateTo: {
|
||||
version: '2.0.0',
|
||||
url: `http://localhost:${port}/update-v2.msix`,
|
||||
name: '2.0.0',
|
||||
notes: 'Test release notes for static format',
|
||||
pub_date: fixedPubDate
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
server.get('/update-v2.msix', (req, res) => {
|
||||
res.download(MSIX_V2);
|
||||
});
|
||||
|
||||
const launchResult = await launchApp(ELECTRON_MSIX_ALIAS, [MAIN_JS_PATH, '--checkUpdate', `http://localhost:${port}/update-check`]);
|
||||
|
||||
logOnError(launchResult, () => {
|
||||
expect(launchResult.code).to.equal(0);
|
||||
expect(requests.length).to.be.greaterThan(0);
|
||||
expect(launchResult.out).to.include('Checking for update...');
|
||||
expect(launchResult.out).to.include('Update available');
|
||||
expect(launchResult.out).to.include('Update downloaded');
|
||||
expect(launchResult.out).to.include('Release Name: 2.0.0');
|
||||
expect(launchResult.out).to.include('Release Notes: Test release notes for static format');
|
||||
expect(launchResult.out).to.include(`Release Date: ${expectedDateStr}`);
|
||||
expect(launchResult.out).to.include(`Update URL: http://localhost:${port}/update-v2.msix`);
|
||||
});
|
||||
|
||||
const updatedVersion = await getMsixPackageVersion('com.electron.myapp');
|
||||
expect(updatedVersion).to.equal('2.0.0.0');
|
||||
});
|
||||
|
||||
it('should not update with update File JSON Format if currentRelease is older than installed version', async () => {
|
||||
await installMsixPackage(MSIX_V2);
|
||||
|
||||
server.get('/update-check', (req, res) => {
|
||||
res.json({
|
||||
currentRelease: '1.0.0',
|
||||
releases: [
|
||||
{
|
||||
version: '1.0.0',
|
||||
updateTo: {
|
||||
version: '1.0.0',
|
||||
url: `http://localhost:${port}/update-v1.msix`,
|
||||
name: '1.0.0',
|
||||
notes: 'Initial release',
|
||||
pub_date: '2010-10-10T10:10:10.000Z'
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
const launchResult = await launchApp(ELECTRON_MSIX_ALIAS, [MAIN_JS_PATH, '--checkUpdate', `http://localhost:${port}/update-check`]);
|
||||
|
||||
logOnError(launchResult, () => {
|
||||
expect(launchResult.code).to.equal(0);
|
||||
expect(requests.length).to.be.greaterThan(0);
|
||||
expect(launchResult.out).to.include('Checking for update...');
|
||||
expect(launchResult.out).to.include('Update not available');
|
||||
});
|
||||
});
|
||||
|
||||
it('should downgrade to older version with JSON server format and allowAnyVersion is true', async () => {
|
||||
await installMsixPackage(MSIX_V2);
|
||||
const initialVersion = await getMsixPackageVersion('com.electron.myapp');
|
||||
expect(initialVersion).to.equal('2.0.0.0');
|
||||
|
||||
const fixedPubDate = '2010-10-10T10:10:10.000Z';
|
||||
const expectedDateStr = new Date(fixedPubDate).toDateString();
|
||||
|
||||
server.get('/update-check', (req, res) => {
|
||||
res.json({
|
||||
url: `http://localhost:${port}/update-v1.msix`,
|
||||
name: '1.0.0',
|
||||
notes: 'Initial release',
|
||||
pub_date: fixedPubDate
|
||||
});
|
||||
});
|
||||
|
||||
server.get('/update-v1.msix', (req, res) => {
|
||||
res.download(MSIX_V1);
|
||||
});
|
||||
|
||||
const launchResult = await launchApp(ELECTRON_MSIX_ALIAS, [MAIN_JS_PATH, '--checkUpdate', `http://localhost:${port}/update-check`, '--allowAnyVersion']);
|
||||
|
||||
logOnError(launchResult, () => {
|
||||
expect(launchResult.code).to.equal(0);
|
||||
expect(requests.length).to.be.greaterThan(0);
|
||||
expect(launchResult.out).to.include('Checking for update...');
|
||||
expect(launchResult.out).to.include('Update available');
|
||||
expect(launchResult.out).to.include('Update downloaded');
|
||||
expect(launchResult.out).to.include('Release Name: 1.0.0');
|
||||
expect(launchResult.out).to.include('Release Notes: Initial release');
|
||||
expect(launchResult.out).to.include(`Release Date: ${expectedDateStr}`);
|
||||
expect(launchResult.out).to.include(`Update URL: http://localhost:${port}/update-v1.msix`);
|
||||
});
|
||||
|
||||
const downgradedVersion = await getMsixPackageVersion('com.electron.myapp');
|
||||
expect(downgradedVersion).to.equal('1.0.0.0');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5888,6 +5888,23 @@ describe('BrowserWindow module', () => {
|
||||
});
|
||||
});
|
||||
|
||||
ifdescribe(process.platform === 'linux')('menu bar AltGr behavior', () => {
|
||||
it('does not toggle auto-hide menu bar visibility', async () => {
|
||||
const w = new BrowserWindow({ show: false, autoHideMenuBar: true });
|
||||
w.setMenuBarVisibility(false);
|
||||
expect(w.isMenuBarVisible()).to.be.false('isMenuBarVisible');
|
||||
|
||||
w.show();
|
||||
await once(w, 'show');
|
||||
w.webContents.focus();
|
||||
w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'AltGr' });
|
||||
w.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'AltGr' });
|
||||
await setTimeout();
|
||||
|
||||
expect(w.isMenuBarVisible()).to.be.false('isMenuBarVisible');
|
||||
});
|
||||
});
|
||||
|
||||
ifdescribe(process.platform !== 'darwin')('when fullscreen state is changed', () => {
|
||||
it('correctly remembers state prior to fullscreen change', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
|
||||
@@ -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' }
|
||||
]);
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3203,6 +3203,29 @@ describe('chromium features', () => {
|
||||
expect(rgb).to.equal('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('long-animation-frame', () => {
|
||||
it('should include script attribution on custom protocols if AlwaysLogLOAFURL is enabled', async () => {
|
||||
const rc = await startRemoteControlApp(['--enable-features=AlwaysLogLOAFURL']);
|
||||
const hasAttribution = await rc.remotely(async (fixture: string) => {
|
||||
const { BrowserWindow, protocol, net } = require('electron/main');
|
||||
const { pathToFileURL } = require('node:url');
|
||||
|
||||
protocol.handle('custom', () => net.fetch(pathToFileURL(fixture).toString()));
|
||||
|
||||
// `show: true` is necessary on Windows and Linux due to https://github.com/electron/electron/issues/32001
|
||||
const w = new BrowserWindow({ show: true });
|
||||
await w.loadURL('custom://my-url');
|
||||
|
||||
const hasAttribution = await w.webContents.executeJavaScript('hasAttributionPromise');
|
||||
|
||||
global.setTimeout(() => require('electron').app.quit());
|
||||
|
||||
return hasAttribution;
|
||||
}, path.join(fixturesPath, 'chromium', 'long-animation-frame.html'));
|
||||
expect(hasAttribution).to.be.true();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('font fallback', () => {
|
||||
|
||||
50
spec/fixtures/api/autoupdater/msix/ElectronDevAppxManifest.xml
vendored
Normal file
50
spec/fixtures/api/autoupdater/msix/ElectronDevAppxManifest.xml
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Package
|
||||
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
||||
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
||||
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
|
||||
xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
|
||||
xmlns:desktop2="http://schemas.microsoft.com/appx/manifest/desktop/windows10/2"
|
||||
IgnorableNamespaces="uap uap3 desktop2">
|
||||
<Identity Name="Electron.Dev.MSIX"
|
||||
ProcessorArchitecture="x64"
|
||||
Version="1.0.0.0"
|
||||
Publisher="CN=Electron"/>
|
||||
<Properties>
|
||||
<DisplayName>Electron Dev MSIX</DisplayName>
|
||||
<PublisherDisplayName>Electron</PublisherDisplayName>
|
||||
<Logo>assets\icon.png</Logo>
|
||||
</Properties>
|
||||
<Dependencies>
|
||||
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.17763.0" />
|
||||
</Dependencies>
|
||||
<Resources>
|
||||
<Resource Language="en-US" />
|
||||
</Resources>
|
||||
<Applications>
|
||||
<Application Id="ElectronMSIX" Executable="Electron.exe" EntryPoint="Windows.FullTrustApplication">
|
||||
<uap:VisualElements
|
||||
DisplayName="Electron Dev MSIX"
|
||||
Description="Electron running with Identity"
|
||||
Square44x44Logo="assets\Square44x44Logo.png"
|
||||
Square150x150Logo="assets\Square150x150Logo.png"
|
||||
BackgroundColor="transparent">
|
||||
</uap:VisualElements>
|
||||
<Extensions>
|
||||
<uap3:Extension
|
||||
Category="windows.appExecutionAlias"
|
||||
Executable="Electron.exe"
|
||||
EntryPoint="Windows.FullTrustApplication">
|
||||
<uap3:AppExecutionAlias>
|
||||
<desktop:ExecutionAlias Alias="ElectronMSIX.exe" />
|
||||
</uap3:AppExecutionAlias>
|
||||
</uap3:Extension>
|
||||
</Extensions>
|
||||
</Application>
|
||||
</Applications>
|
||||
<Capabilities>
|
||||
<rescap:Capability Name="runFullTrust" />
|
||||
<Capability Name="internetClient" />
|
||||
</Capabilities>
|
||||
</Package>
|
||||
BIN
spec/fixtures/api/autoupdater/msix/HelloMSIX_V1.msix
vendored
Normal file
BIN
spec/fixtures/api/autoupdater/msix/HelloMSIX_V1.msix
vendored
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user