mirror of
https://github.com/electron/electron.git
synced 2026-02-19 03:14:51 -05:00
chore: handle patch merge conflicts
This commit is contained in:
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -11,6 +11,7 @@ Contributors guide: https://github.com/electron/electron/blob/main/CONTRIBUTING.
|
||||
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->
|
||||
|
||||
- [ ] PR description included
|
||||
- [ ] I have built and tested this PR
|
||||
- [ ] `npm test` passes
|
||||
- [ ] tests are [changed or added](https://github.com/electron/electron/blob/main/docs/development/testing.md)
|
||||
- [ ] relevant API documentation, tutorials, and examples are updated and follow the [documentation style guide](https://github.com/electron/electron/blob/main/docs/development/style-guide.md)
|
||||
|
||||
11
.github/actions/build-electron/action.yml
vendored
11
.github/actions/build-electron/action.yml
vendored
@@ -26,6 +26,9 @@ inputs:
|
||||
is-asan:
|
||||
description: 'The ASan Linux build'
|
||||
required: false
|
||||
upload-out-gen-artifacts:
|
||||
description: 'Whether to upload the out/${dir}/gen artifacts'
|
||||
required: false
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
@@ -37,7 +40,7 @@ runs:
|
||||
echo "GN_EXTRA_ARGS=$GN_APPENDED_ARGS" >> $GITHUB_ENV
|
||||
- name: Set GN_EXTRA_ARGS for Windows
|
||||
shell: bash
|
||||
if: ${{inputs.target-arch != 'x64' && inputs.target-platform == 'win' }}
|
||||
if: ${{ inputs.target-arch != 'x64' && inputs.target-platform == 'win' }}
|
||||
run: |
|
||||
GN_APPENDED_ARGS="$GN_EXTRA_ARGS target_cpu=\"${{ inputs.target-arch }}\""
|
||||
echo "GN_EXTRA_ARGS=$GN_APPENDED_ARGS" >> $GITHUB_ENV
|
||||
@@ -274,3 +277,9 @@ runs:
|
||||
with:
|
||||
name: src_artifacts_${{ env.ARTIFACT_KEY }}
|
||||
path: ./src_artifacts_${{ inputs.artifact-platform }}_${{ inputs.target-arch }}
|
||||
- name: Upload Out Gen Artifacts ${{ inputs.step-suffix }}
|
||||
if: ${{ inputs.upload-out-gen-artifacts == 'true' }}
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808
|
||||
with:
|
||||
name: out_gen_artifacts_${{ env.ARTIFACT_KEY }}
|
||||
path: ./src/out/Default/gen
|
||||
|
||||
72
.github/workflows/apply-patches.yml
vendored
Normal file
72
.github/workflows/apply-patches.yml
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
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.head.sha }}
|
||||
- name: Rebase onto Base Branch
|
||||
working-directory: src/electron
|
||||
env:
|
||||
BASE_REF: ${{ github.event.pull_request.base.ref }}
|
||||
run: |
|
||||
git config user.email "electron@github.com"
|
||||
git config user.name "Electron Bot"
|
||||
git fetch origin ${BASE_REF}
|
||||
git rebase origin/${BASE_REF}
|
||||
- name: Checkout & Sync & Save
|
||||
uses: ./src/electron/.github/actions/checkout
|
||||
with:
|
||||
target-platform: linux
|
||||
2
.github/workflows/archaeologist-dig.yml
vendored
2
.github/workflows/archaeologist-dig.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout Electron
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Node.js/npm
|
||||
|
||||
3
.github/workflows/audit-branch-ci.yml
vendored
3
.github/workflows/audit-branch-ci.yml
vendored
@@ -11,6 +11,7 @@ permissions: {}
|
||||
jobs:
|
||||
audit_branch_ci:
|
||||
name: Audit CI on Branches
|
||||
if: github.repository == 'electron/electron'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -20,7 +21,7 @@ jobs:
|
||||
with:
|
||||
node-version: 22.17.x
|
||||
- name: Sparse checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.
|
||||
|
||||
2
.github/workflows/branch-created.yml
vendored
2
.github/workflows/branch-created.yml
vendored
@@ -14,7 +14,7 @@ permissions: {}
|
||||
jobs:
|
||||
release-branch-created:
|
||||
name: Release Branch Created
|
||||
if: ${{ github.event_name == 'workflow_dispatch' || (github.event.ref_type == 'branch' && endsWith(github.event.ref, '-x-y') && !startsWith(github.event.ref, 'roller')) }}
|
||||
if: ${{ github.repository == 'electron/electron' && (github.event_name == 'workflow_dispatch' || (github.event.ref_type == 'branch' && endsWith(github.event.ref, '-x-y') && !startsWith(github.event.ref, 'roller'))) }}
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
9
.github/workflows/build-git-cache.yml
vendored
9
.github/workflows/build-git-cache.yml
vendored
@@ -10,6 +10,7 @@ permissions: {}
|
||||
|
||||
jobs:
|
||||
build-git-cache-linux:
|
||||
if: github.repository == 'electron/electron'
|
||||
runs-on: electron-arc-centralus-linux-amd64-32core
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -23,7 +24,7 @@ jobs:
|
||||
GCLIENT_EXTRA_ARGS: '--custom-var=checkout_arm=True --custom-var=checkout_arm64=True'
|
||||
steps:
|
||||
- name: Checkout Electron
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
with:
|
||||
path: src/electron
|
||||
fetch-depth: 0
|
||||
@@ -33,6 +34,7 @@ jobs:
|
||||
target-platform: linux
|
||||
|
||||
build-git-cache-windows:
|
||||
if: github.repository == 'electron/electron'
|
||||
runs-on: electron-arc-centralus-linux-amd64-32core
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -47,7 +49,7 @@ jobs:
|
||||
TARGET_OS: 'win'
|
||||
steps:
|
||||
- name: Checkout Electron
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
with:
|
||||
path: src/electron
|
||||
fetch-depth: 0
|
||||
@@ -57,6 +59,7 @@ jobs:
|
||||
target-platform: win
|
||||
|
||||
build-git-cache-macos:
|
||||
if: github.repository == 'electron/electron'
|
||||
runs-on: electron-arc-centralus-linux-amd64-32core
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -72,7 +75,7 @@ jobs:
|
||||
GCLIENT_EXTRA_ARGS: '--custom-var=checkout_mac=True --custom-var=host_os=mac'
|
||||
steps:
|
||||
- name: Checkout Electron
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
with:
|
||||
path: src/electron
|
||||
fetch-depth: 0
|
||||
|
||||
15
.github/workflows/build.yml
vendored
15
.github/workflows/build.yml
vendored
@@ -47,6 +47,7 @@ permissions: {}
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
if: github.repository == 'electron/electron'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -57,7 +58,7 @@ jobs:
|
||||
build-image-sha: ${{ steps.set-output.outputs.build-image-sha }}
|
||||
docs-only: ${{ steps.set-output.outputs.docs-only }}
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
@@ -124,7 +125,7 @@ jobs:
|
||||
build-image-sha: ${{ needs.setup.outputs.build-image-sha }}
|
||||
steps:
|
||||
- name: Checkout Electron
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
with:
|
||||
path: src/electron
|
||||
fetch-depth: 0
|
||||
@@ -156,7 +157,7 @@ jobs:
|
||||
build-image-sha: ${{ needs.setup.outputs.build-image-sha}}
|
||||
steps:
|
||||
- name: Checkout Electron
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
with:
|
||||
path: src/electron
|
||||
fetch-depth: 0
|
||||
@@ -188,7 +189,7 @@ jobs:
|
||||
build-image-sha: ${{ needs.setup.outputs.build-image-sha}}
|
||||
steps:
|
||||
- name: Checkout Electron
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
with:
|
||||
path: src/electron
|
||||
fetch-depth: 0
|
||||
@@ -283,13 +284,15 @@ jobs:
|
||||
contents: read
|
||||
issues: read
|
||||
pull-requests: read
|
||||
uses: ./.github/workflows/pipeline-electron-build-and-test-and-nan.yml
|
||||
uses: ./.github/workflows/pipeline-electron-build-and-tidy-and-test-and-nan.yml
|
||||
needs: checkout-linux
|
||||
if: ${{ needs.setup.outputs.src == 'true' }}
|
||||
with:
|
||||
build-runs-on: electron-arc-centralus-linux-amd64-32core
|
||||
clang-tidy-runs-on: electron-arc-centralus-linux-amd64-8core
|
||||
test-runs-on: electron-arc-centralus-linux-amd64-4core
|
||||
build-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"]}'
|
||||
clang-tidy-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"]}'
|
||||
test-container: '{"image":"ghcr.io/electron/build:${{ needs.checkout-linux.outputs.build-image-sha }}","options":"--user root --privileged --init"}'
|
||||
target-platform: linux
|
||||
target-arch: x64
|
||||
@@ -426,7 +429,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
needs: [docs-only, macos-x64, macos-arm64, linux-x64, linux-x64-asan, linux-arm, linux-arm64, windows-x64, windows-x86, windows-arm64]
|
||||
if: always() && !contains(needs.*.result, 'failure')
|
||||
if: always() && github.repository == 'electron/electron' && !contains(needs.*.result, 'failure')
|
||||
steps:
|
||||
- name: GitHub Actions Jobs Done
|
||||
run: |
|
||||
|
||||
1
.github/workflows/clean-src-cache.yml
vendored
1
.github/workflows/clean-src-cache.yml
vendored
@@ -12,6 +12,7 @@ permissions: {}
|
||||
|
||||
jobs:
|
||||
clean-src-cache:
|
||||
if: github.repository == 'electron/electron'
|
||||
runs-on: electron-arc-centralus-linux-amd64-32core
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
2
.github/workflows/issue-opened.yml
vendored
2
.github/workflows/issue-opened.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
creds: ${{ secrets.ISSUE_TRIAGE_GH_APP_CREDS }}
|
||||
org: electron
|
||||
- name: Sparse checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.
|
||||
|
||||
3
.github/workflows/linux-publish.yml
vendored
3
.github/workflows/linux-publish.yml
vendored
@@ -21,6 +21,7 @@ permissions: {}
|
||||
|
||||
jobs:
|
||||
checkout-linux:
|
||||
if: github.repository == 'electron/electron'
|
||||
runs-on: electron-arc-centralus-linux-amd64-32core
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -35,7 +36,7 @@ jobs:
|
||||
GCLIENT_EXTRA_ARGS: '--custom-var=checkout_arm=True --custom-var=checkout_arm64=True'
|
||||
steps:
|
||||
- name: Checkout Electron
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
with:
|
||||
path: src/electron
|
||||
fetch-depth: 0
|
||||
|
||||
3
.github/workflows/macos-disk-cleanup.yml
vendored
3
.github/workflows/macos-disk-cleanup.yml
vendored
@@ -13,6 +13,7 @@ permissions: {}
|
||||
|
||||
jobs:
|
||||
macos-disk-cleanup:
|
||||
if: github.repository == 'electron/electron'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -25,7 +26,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.github/actions/free-space-macos
|
||||
|
||||
3
.github/workflows/macos-publish.yml
vendored
3
.github/workflows/macos-publish.yml
vendored
@@ -22,6 +22,7 @@ permissions: {}
|
||||
|
||||
jobs:
|
||||
checkout-macos:
|
||||
if: github.repository == 'electron/electron'
|
||||
runs-on: electron-arc-centralus-linux-amd64-32core
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -36,7 +37,7 @@ jobs:
|
||||
GCLIENT_EXTRA_ARGS: '--custom-var=checkout_mac=True --custom-var=host_os=mac'
|
||||
steps:
|
||||
- name: Checkout Electron
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
with:
|
||||
path: src/electron
|
||||
fetch-depth: 0
|
||||
|
||||
124
.github/workflows/pipeline-electron-build-and-tidy-and-test-and-nan.yml
vendored
Normal file
124
.github/workflows/pipeline-electron-build-and-tidy-and-test-and-nan.yml
vendored
Normal file
@@ -0,0 +1,124 @@
|
||||
name: Electron Build & Clang Tidy & Test (+ Node + NaN) Pipeline
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
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 or arm'
|
||||
required: true
|
||||
build-runs-on:
|
||||
type: string
|
||||
description: 'What host to run the build'
|
||||
required: true
|
||||
clang-tidy-runs-on:
|
||||
type: string
|
||||
description: 'What host to run clang-tidy on'
|
||||
required: true
|
||||
test-runs-on:
|
||||
type: string
|
||||
description: 'What host to run the tests on'
|
||||
required: true
|
||||
build-container:
|
||||
type: string
|
||||
description: 'JSON container information for aks runs-on'
|
||||
required: false
|
||||
default: '{"image":null}'
|
||||
clang-tidy-container:
|
||||
type: string
|
||||
description: 'JSON container information to run clang-tidy on'
|
||||
required: false
|
||||
default: '{"image":null}'
|
||||
test-container:
|
||||
type: string
|
||||
description: 'JSON container information for testing'
|
||||
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
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: electron-build-and-test-and-nan-${{ inputs.target-platform }}-${{ inputs.target-arch }}-${{ github.ref_protected == true && github.run_id || github.ref }}
|
||||
cancel-in-progress: ${{ github.ref_protected != true }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: ./.github/workflows/pipeline-segment-electron-build.yml
|
||||
permissions:
|
||||
contents: read
|
||||
with:
|
||||
build-runs-on: ${{ inputs.build-runs-on }}
|
||||
build-container: ${{ inputs.build-container }}
|
||||
target-platform: ${{ inputs.target-platform }}
|
||||
target-arch: ${{ inputs.target-arch }}
|
||||
is-release: ${{ inputs.is-release }}
|
||||
gn-build-type: ${{ inputs.gn-build-type }}
|
||||
generate-symbols: ${{ inputs.generate-symbols }}
|
||||
upload-to-storage: ${{ inputs.upload-to-storage }}
|
||||
upload-out-gen-artifacts: true
|
||||
secrets: inherit
|
||||
clang-tidy:
|
||||
uses: ./.github/workflows/pipeline-segment-electron-clang-tidy.yml
|
||||
permissions:
|
||||
contents: read
|
||||
needs: build
|
||||
with:
|
||||
clang-tidy-runs-on: ${{ inputs.clang-tidy-runs-on }}
|
||||
clang-tidy-container: ${{ inputs.clang-tidy-container }}
|
||||
target-platform: ${{ inputs.target-platform }}
|
||||
target-arch: ${{ inputs.target-arch }}
|
||||
secrets: inherit
|
||||
test:
|
||||
uses: ./.github/workflows/pipeline-segment-electron-test.yml
|
||||
permissions:
|
||||
contents: read
|
||||
issues: read
|
||||
pull-requests: read
|
||||
needs: build
|
||||
with:
|
||||
target-arch: ${{ inputs.target-arch }}
|
||||
target-platform: ${{ inputs.target-platform }}
|
||||
test-runs-on: ${{ inputs.test-runs-on }}
|
||||
test-container: ${{ inputs.test-container }}
|
||||
secrets: inherit
|
||||
nn-test:
|
||||
uses: ./.github/workflows/pipeline-segment-node-nan-test.yml
|
||||
permissions:
|
||||
contents: read
|
||||
needs: build
|
||||
with:
|
||||
target-arch: ${{ inputs.target-arch }}
|
||||
target-platform: ${{ inputs.target-platform }}
|
||||
test-runs-on: ${{ inputs.test-runs-on }}
|
||||
test-container: ${{ inputs.test-container }}
|
||||
gn-build-type: ${{ inputs.gn-build-type }}
|
||||
secrets: inherit
|
||||
121
.github/workflows/pipeline-electron-build-and-tidy-and-test.yml
vendored
Normal file
121
.github/workflows/pipeline-electron-build-and-tidy-and-test.yml
vendored
Normal file
@@ -0,0 +1,121 @@
|
||||
name: Electron Build & Clang Tidy & Test Pipeline
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
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 or arm'
|
||||
required: true
|
||||
build-runs-on:
|
||||
type: string
|
||||
description: 'What host to run the build'
|
||||
required: true
|
||||
clang-tidy-runs-on:
|
||||
type: string
|
||||
description: 'What host to run clang-tidy on'
|
||||
required: true
|
||||
test-runs-on:
|
||||
type: string
|
||||
description: 'What host to run the tests on'
|
||||
required: true
|
||||
build-container:
|
||||
type: string
|
||||
description: 'JSON container information for aks runs-on'
|
||||
required: false
|
||||
default: '{"image":null}'
|
||||
clang-tidy-container:
|
||||
type: string
|
||||
description: 'JSON container information to run clang-tidy on'
|
||||
required: false
|
||||
default: '{"image":null}'
|
||||
test-container:
|
||||
type: string
|
||||
description: 'JSON container information for testing'
|
||||
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
|
||||
|
||||
concurrency:
|
||||
group: electron-build-and-tidy-and-test-${{ inputs.target-platform }}-${{ inputs.target-arch }}-${{ github.ref_protected == true && github.run_id || github.ref }}
|
||||
cancel-in-progress: ${{ github.ref_protected != true }}
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: ./.github/workflows/pipeline-segment-electron-build.yml
|
||||
permissions:
|
||||
contents: read
|
||||
with:
|
||||
build-runs-on: ${{ inputs.build-runs-on }}
|
||||
build-container: ${{ inputs.build-container }}
|
||||
target-platform: ${{ inputs.target-platform }}
|
||||
target-arch: ${{ inputs.target-arch }}
|
||||
is-release: ${{ inputs.is-release }}
|
||||
gn-build-type: ${{ inputs.gn-build-type }}
|
||||
generate-symbols: ${{ inputs.generate-symbols }}
|
||||
upload-to-storage: ${{ inputs.upload-to-storage }}
|
||||
is-asan: ${{ inputs.is-asan }}
|
||||
enable-ssh: ${{ inputs.enable-ssh }}
|
||||
upload-out-gen-artifacts: true
|
||||
secrets: inherit
|
||||
clang-tidy:
|
||||
uses: ./.github/workflows/pipeline-segment-electron-clang-tidy.yml
|
||||
permissions:
|
||||
contents: read
|
||||
needs: build
|
||||
with:
|
||||
clang-tidy-runs-on: ${{ inputs.clang-tidy-runs-on }}
|
||||
clang-tidy-container: ${{ inputs.clang-tidy-container }}
|
||||
target-platform: ${{ inputs.target-platform }}
|
||||
target-arch: ${{ inputs.target-arch }}
|
||||
secrets: inherit
|
||||
test:
|
||||
uses: ./.github/workflows/pipeline-segment-electron-test.yml
|
||||
permissions:
|
||||
contents: read
|
||||
issues: read
|
||||
pull-requests: read
|
||||
needs: build
|
||||
with:
|
||||
target-arch: ${{ inputs.target-arch }}
|
||||
target-platform: ${{ inputs.target-platform }}
|
||||
test-runs-on: ${{ inputs.test-runs-on }}
|
||||
test-container: ${{ inputs.test-container }}
|
||||
is-asan: ${{ inputs.is-asan }}
|
||||
enable-ssh: ${{ inputs.enable-ssh }}
|
||||
secrets: inherit
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
container: ${{ fromJSON(inputs.container) }}
|
||||
steps:
|
||||
- name: Checkout Electron
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
with:
|
||||
path: src/electron
|
||||
fetch-depth: 0
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
with:
|
||||
target-platform: linux
|
||||
- name: Checkout Electron
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
with:
|
||||
path: src/electron
|
||||
fetch-depth: 0
|
||||
|
||||
2
.github/workflows/pipeline-electron-lint.yml
vendored
2
.github/workflows/pipeline-electron-lint.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
container: ${{ fromJSON(inputs.container) }}
|
||||
steps:
|
||||
- name: Checkout Electron
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
with:
|
||||
path: src/electron
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -53,6 +53,11 @@ on:
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
upload-out-gen-artifacts:
|
||||
description: 'Whether to upload the src/gen artifacts'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
enable-ssh:
|
||||
description: 'Enable SSH debugging'
|
||||
required: false
|
||||
@@ -73,7 +78,12 @@ env:
|
||||
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' }}
|
||||
GCLIENT_EXTRA_ARGS: |-
|
||||
${{ case(
|
||||
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 }}
|
||||
|
||||
@@ -95,7 +105,7 @@ jobs:
|
||||
run: |
|
||||
mkdir src
|
||||
- name: Checkout Electron
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
with:
|
||||
path: src/electron
|
||||
fetch-depth: 0
|
||||
@@ -163,7 +173,7 @@ jobs:
|
||||
if: ${{ inputs.target-platform == 'linux' }}
|
||||
uses: ./src/electron/.github/actions/restore-cache-aks
|
||||
- name: Checkout Electron
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
with:
|
||||
path: src/electron
|
||||
fetch-depth: 0
|
||||
@@ -196,11 +206,12 @@ jobs:
|
||||
with:
|
||||
target-arch: ${{ inputs.target-arch }}
|
||||
target-platform: ${{ inputs.target-platform }}
|
||||
artifact-platform: ${{ inputs.target-platform == 'macos' && 'darwin' || inputs.target-platform }}
|
||||
artifact-platform: ${{ case(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 }}'
|
||||
upload-out-gen-artifacts: '${{ inputs.upload-out-gen-artifacts }}'
|
||||
- name: Set GN_EXTRA_ARGS for MAS Build
|
||||
if: ${{ inputs.target-platform == 'macos' && (inputs.target-variant == 'all' || inputs.target-variant == 'mas') }}
|
||||
run: |
|
||||
|
||||
164
.github/workflows/pipeline-segment-electron-clang-tidy.yml
vendored
Normal file
164
.github/workflows/pipeline-segment-electron-clang-tidy.yml
vendored
Normal file
@@ -0,0 +1,164 @@
|
||||
name: Pipeline Segment - Electron Clang-Tidy
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
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 or arm'
|
||||
required: true
|
||||
clang-tidy-runs-on:
|
||||
type: string
|
||||
description: 'What host to run clang-tidy on'
|
||||
required: true
|
||||
clang-tidy-container:
|
||||
type: string
|
||||
description: 'JSON container information for aks runs-on'
|
||||
required: false
|
||||
default: '{"image":null}'
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: electron-clang-tidy-${{ inputs.target-platform }}-${{ inputs.target-arch }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
GCLIENT_EXTRA_ARGS: |-
|
||||
${{ case(
|
||||
inputs.target-platform == 'macos', '--custom-var=checkout_mac=True --custom-var=host_os=mac',
|
||||
inputs.target-platform == 'linux', '--custom-var=checkout_arm=True --custom-var=checkout_arm64=True',
|
||||
'--custom-var=checkout_win=True'
|
||||
) }}
|
||||
ELECTRON_OUT_DIR: Default
|
||||
|
||||
jobs:
|
||||
clang-tidy:
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
runs-on: ${{ inputs.clang-tidy-runs-on }}
|
||||
permissions:
|
||||
contents: read
|
||||
container: ${{ fromJSON(inputs.clang-tidy-container) }}
|
||||
env:
|
||||
BUILD_TYPE: ${{ case(inputs.target-platform == 'macos', 'darwin', inputs.target-platform) }}
|
||||
TARGET_ARCH: ${{ inputs.target-arch }}
|
||||
TARGET_PLATFORM: ${{ inputs.target-platform }}
|
||||
ARTIFACT_KEY: ${{ case(inputs.target-platform == 'macos', 'darwin', inputs.target-platform) }}_${{ inputs.target-arch }}
|
||||
steps:
|
||||
- name: Checkout Electron
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
with:
|
||||
path: src/electron
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Cleanup disk space on macOS
|
||||
if: ${{ inputs.target-platform == 'macos' }}
|
||||
shell: bash
|
||||
run: |
|
||||
sudo mkdir -p $TMPDIR/del-target
|
||||
|
||||
tmpify() {
|
||||
if [ -d "$1" ]; then
|
||||
sudo mv "$1" $TMPDIR/del-target/$(echo $1|shasum -a 256|head -n1|cut -d " " -f1)
|
||||
fi
|
||||
}
|
||||
tmpify /Library/Developer/CoreSimulator
|
||||
tmpify ~/Library/Developer/CoreSimulator
|
||||
sudo rm -rf $TMPDIR/del-target
|
||||
- name: Check disk space after freeing up space
|
||||
if: ${{ inputs.target-platform == 'macos' }}
|
||||
run: df -h
|
||||
- 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: Enable windows toolchain
|
||||
if: ${{ inputs.target-platform == 'win' }}
|
||||
run: |
|
||||
echo "ELECTRON_DEPOT_TOOLS_WIN_TOOLCHAIN=1" >> $GITHUB_ENV
|
||||
- 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 == 'macos' }}
|
||||
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' || inputs.target-platform == 'win' }}
|
||||
uses: ./src/electron/.github/actions/restore-cache-aks
|
||||
with:
|
||||
target-platform: ${{ inputs.target-platform }}
|
||||
- name: Run Electron Only Hooks
|
||||
run: |
|
||||
echo "solutions=[{'name':'src/electron','url':None,'deps_file':'DEPS','custom_vars':{'process_deps':False},'managed':False}]" > tmpgclient
|
||||
if [ "${{ inputs.target-platform }}" = "win" ]; then
|
||||
echo "solutions=[{'name':'src/electron','url':None,'deps_file':'DEPS','custom_vars':{'process_deps':False,'install_sysroot':False,'checkout_win':True},'managed':False}]" > tmpgclient
|
||||
echo "target_os=['win']" >> tmpgclient
|
||||
fi
|
||||
e d gclient runhooks --gclientfile=tmpgclient
|
||||
|
||||
# Fix VS Toolchain
|
||||
if [ "${{ inputs.target-platform }}" = "win" ]; then
|
||||
rm -rf src/third_party/depot_tools/win_toolchain/vs_files
|
||||
e d python3 src/build/vs_toolchain.py update --force
|
||||
fi
|
||||
- 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: Checkout Electron
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
with:
|
||||
path: src/electron
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Install Dependencies
|
||||
uses: ./src/electron/.github/actions/install-dependencies
|
||||
- name: Default GN gen
|
||||
run: |
|
||||
cd src/electron
|
||||
git pack-refs
|
||||
- name: Download Out Gen Artifacts
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131
|
||||
with:
|
||||
name: out_gen_artifacts_${{ env.ARTIFACT_KEY }}
|
||||
path: ./src/out/${{ env.ELECTRON_OUT_DIR }}/gen
|
||||
- name: Add Clang problem matcher
|
||||
shell: bash
|
||||
run: echo "::add-matcher::src/electron/.github/problem-matchers/clang.json"
|
||||
- name: Run Clang-Tidy
|
||||
run: |
|
||||
e init -f --root=$(pwd) --out=${ELECTRON_OUT_DIR} testing --target-cpu ${TARGET_ARCH}
|
||||
|
||||
export GN_EXTRA_ARGS="target_cpu=\"${TARGET_ARCH}\""
|
||||
if [ "${{ inputs.target-platform }}" = "win" ]; then
|
||||
export GN_EXTRA_ARGS="$GN_EXTRA_ARGS use_v8_context_snapshot=true target_os=\"win\""
|
||||
fi
|
||||
|
||||
e build --only-gen
|
||||
|
||||
cd src/electron
|
||||
node script/yarn.js lint:clang-tidy --jobs 8 --out-dir ../out/${ELECTRON_OUT_DIR}
|
||||
- name: Remove Clang problem matcher
|
||||
shell: bash
|
||||
run: echo "::remove-matcher owner=clang::"
|
||||
- name: Wait for active SSH sessions
|
||||
if: always() && !cancelled()
|
||||
shell: bash
|
||||
run: |
|
||||
while [ -f /var/.ssh-lock ]
|
||||
do
|
||||
sleep 60
|
||||
done
|
||||
@@ -34,7 +34,12 @@ concurrency:
|
||||
|
||||
env:
|
||||
ELECTRON_RBE_JWT: ${{ secrets.ELECTRON_RBE_JWT }}
|
||||
GCLIENT_EXTRA_ARGS: ${{ inputs.target-platform == 'macos' && '--custom-var=checkout_mac=True --custom-var=host_os=mac' || (inputs.target-platform == 'linux' && '--custom-var=checkout_arm=True --custom-var=checkout_arm64=True' || '--custom-var=checkout_win=True') }}
|
||||
GCLIENT_EXTRA_ARGS: |-
|
||||
${{ case(
|
||||
inputs.target-platform == 'macos', '--custom-var=checkout_mac=True --custom-var=host_os=mac',
|
||||
inputs.target-platform == 'linux', '--custom-var=checkout_arm=True --custom-var=checkout_arm64=True',
|
||||
'--custom-var=checkout_win=True'
|
||||
) }}
|
||||
ELECTRON_OUT_DIR: Default
|
||||
|
||||
jobs:
|
||||
@@ -48,7 +53,7 @@ jobs:
|
||||
container: ${{ fromJSON(inputs.check-container) }}
|
||||
steps:
|
||||
- name: Checkout Electron
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
with:
|
||||
path: src/electron
|
||||
fetch-depth: 0
|
||||
@@ -115,7 +120,7 @@ jobs:
|
||||
- name: Add CHROMIUM_BUILDTOOLS_PATH to env
|
||||
run: echo "CHROMIUM_BUILDTOOLS_PATH=$(pwd)/src/buildtools" >> $GITHUB_ENV
|
||||
- name: Checkout Electron
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
with:
|
||||
path: src/electron
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -58,8 +58,13 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build-type: ${{ inputs.target-platform == 'macos' && fromJSON('["darwin","mas"]') || (inputs.target-platform == 'win' && fromJSON('["win"]') || fromJSON('["linux"]')) }}
|
||||
shard: ${{ inputs.target-platform == 'linux' && fromJSON('[1, 2, 3]') || fromJSON('[1, 2]') }}
|
||||
build-type: |-
|
||||
${{ case(
|
||||
inputs.target-platform == 'macos', fromJSON('["darwin","mas"]'),
|
||||
inputs.target-platform == 'win', fromJSON('["win"]'),
|
||||
fromJSON('["linux"]')
|
||||
) }}
|
||||
shard: ${{ case(inputs.target-platform == 'linux', fromJSON('[1, 2, 3]'), fromJSON('[1, 2]')) }}
|
||||
env:
|
||||
BUILD_TYPE: ${{ matrix.build-type }}
|
||||
TARGET_ARCH: ${{ inputs.target-arch }}
|
||||
@@ -119,7 +124,7 @@ jobs:
|
||||
if: ${{ inputs.target-platform == 'macos' }}
|
||||
run: sudo xcode-select --switch /Applications/Xcode_16.4.app
|
||||
- name: Checkout Electron
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
with:
|
||||
path: src/electron
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
container: ${{ fromJSON(inputs.test-container) }}
|
||||
steps:
|
||||
- name: Checkout Electron
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
with:
|
||||
path: src/electron
|
||||
fetch-depth: 0
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
container: ${{ fromJSON(inputs.test-container) }}
|
||||
steps:
|
||||
- name: Checkout Electron
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
with:
|
||||
path: src/electron
|
||||
fetch-depth: 0
|
||||
|
||||
31
.github/workflows/pull-request-labeled.yml
vendored
31
.github/workflows/pull-request-labeled.yml
vendored
@@ -44,3 +44,34 @@ jobs:
|
||||
project-number: 94
|
||||
field: Status
|
||||
field-value: ✅ Reviewed
|
||||
pull-request-labeled-ai-pr:
|
||||
name: ai-pr label added
|
||||
if: github.event.label.name == 'ai-pr'
|
||||
runs-on: ubuntu-latest
|
||||
permissions: {}
|
||||
steps:
|
||||
- name: Generate GitHub App token
|
||||
uses: electron/github-app-auth-action@e14e47722ed120360649d0789e25b9baece12725 # v2.0.0
|
||||
id: generate-token
|
||||
with:
|
||||
creds: ${{ secrets.ISSUE_TRIAGE_GH_APP_CREDS }}
|
||||
- name: Create comment
|
||||
uses: actions-cool/issues-helper@e2ff99831a4f13625d35064e2b3dfe65c07a0396 # v3.7.5
|
||||
with:
|
||||
actions: 'create-comment'
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
body: |
|
||||
<!-- ai-pr -->
|
||||
|
||||
*AI PR Detected*
|
||||
|
||||
Hello @${{ github.event.pull_request.user.login }}. Due to the high amount of AI spam PRs we receive, if a PR is detected to be majority AI-generated without disclosure and untested, we will automatically close the PR.
|
||||
|
||||
We welcome the use of AI tools, as long as the PR meets our quality standards and has clearly been built and tested. If you believe your PR was closed in error, we welcome you to resubmit. However, please read our [CONTRIBUTING.md](http://contributing.md/) carefully before reopening. Thanks for your contribution.
|
||||
- name: Close the pull request
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||
GH_REPO: electron/electron
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
gh pr close "$PR_NUMBER"
|
||||
|
||||
71
.github/workflows/rerun-apply-patches.yml
vendored
Normal file
71
.github/workflows/rerun-apply-patches.yml
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
name: Rerun PR Apply Patches
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- '[1-9][0-9]-x-y'
|
||||
paths:
|
||||
- 'DEPS'
|
||||
- 'patches/**'
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
rerun-apply-patches:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: write
|
||||
checks: read
|
||||
contents: read
|
||||
pull-requests: read
|
||||
steps:
|
||||
- name: Find PRs and Rerun Apply Patches
|
||||
env:
|
||||
GH_REPO: ${{ github.repository }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
BRANCH="${GITHUB_REF#refs/heads/}"
|
||||
|
||||
# Find all open PRs targeting this branch
|
||||
PRS=$(gh pr list --base "$BRANCH" --state open --limit 250 --json number)
|
||||
|
||||
echo "$PRS" | jq -c '.[]' | while read -r pr; do
|
||||
PR_NUMBER=$(echo "$pr" | jq -r '.number')
|
||||
echo "Processing PR #${PR_NUMBER}"
|
||||
|
||||
# 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 workflow found for PR #${PR_NUMBER}"
|
||||
continue
|
||||
fi
|
||||
|
||||
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 '.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)
|
||||
|
||||
if [ -z "$RUN_ID" ]; then
|
||||
echo " Could not extract run ID from link: ${LINK}"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Check if the workflow is currently in progress
|
||||
RUN_STATUS=$(gh run view "$RUN_ID" --json status --jq '.status')
|
||||
|
||||
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"
|
||||
done
|
||||
5
.github/workflows/scorecards.yml
vendored
5
.github/workflows/scorecards.yml
vendored
@@ -13,6 +13,7 @@ permissions: read-all
|
||||
jobs:
|
||||
analysis:
|
||||
name: Scorecards analysis
|
||||
if: github.repository == 'electron/electron'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Needed to upload the results to code-scanning dashboard.
|
||||
@@ -22,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -50,6 +51,6 @@ jobs:
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v3.29.5
|
||||
uses: github/codeql-action/upload-sarif@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # v3.29.5
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
1
.github/workflows/stable-prep-items.yml
vendored
1
.github/workflows/stable-prep-items.yml
vendored
@@ -10,6 +10,7 @@ permissions: {}
|
||||
jobs:
|
||||
check-stable-prep-items:
|
||||
name: Check Stable Prep Items
|
||||
if: github.repository == 'electron/electron'
|
||||
runs-on: ubuntu-latest
|
||||
permissions: {}
|
||||
steps:
|
||||
|
||||
3
.github/workflows/stale.yml
vendored
3
.github/workflows/stale.yml
vendored
@@ -9,6 +9,7 @@ permissions: {}
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
if: github.repository == 'electron/electron'
|
||||
runs-on: ubuntu-latest
|
||||
permissions: {}
|
||||
steps:
|
||||
@@ -33,7 +34,7 @@ jobs:
|
||||
pending-repro:
|
||||
runs-on: ubuntu-latest
|
||||
permissions: {}
|
||||
if: ${{ always() }}
|
||||
if: ${{ always() && github.repository == 'electron/electron' }}
|
||||
needs: stale
|
||||
steps:
|
||||
- name: Generate GitHub App token
|
||||
|
||||
3
.github/workflows/windows-publish.yml
vendored
3
.github/workflows/windows-publish.yml
vendored
@@ -22,6 +22,7 @@ permissions: {}
|
||||
|
||||
jobs:
|
||||
checkout-windows:
|
||||
if: github.repository == 'electron/electron'
|
||||
runs-on: electron-arc-centralus-linux-amd64-32core
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -40,7 +41,7 @@ jobs:
|
||||
build-image-sha: ${{ inputs.build-image-sha }}
|
||||
steps:
|
||||
- name: Checkout Electron
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
with:
|
||||
path: src/electron
|
||||
fetch-depth: 0
|
||||
|
||||
32
BUILD.gn
32
BUILD.gn
@@ -420,6 +420,37 @@ action("electron_generate_node_defines") {
|
||||
args = [ rebase_path(target_gen_dir) ] + rebase_path(inputs)
|
||||
}
|
||||
|
||||
# MSIX updater needs to be in a separate source_set because it uses C++/WinRT
|
||||
# headers that require exceptions to be enabled.
|
||||
source_set("electron_msix_updater") {
|
||||
sources = [
|
||||
"shell/browser/api/electron_api_msix_updater.cc",
|
||||
"shell/browser/api/electron_api_msix_updater.h",
|
||||
]
|
||||
|
||||
configs += [ "//third_party/electron_node:node_external_config" ]
|
||||
|
||||
public_configs = [ ":electron_lib_config" ]
|
||||
|
||||
if (is_win) {
|
||||
cflags_cc = [
|
||||
"/EHsc", # Enable C++ exceptions for C++/WinRT
|
||||
"-Wno-c++98-compat-extra-semi", #Suppress C++98 compatibility warnings
|
||||
]
|
||||
|
||||
include_dirs = [ "//third_party/nearby/src/internal/platform/implementation/windows/generated" ]
|
||||
}
|
||||
|
||||
deps = [
|
||||
"//base",
|
||||
"//content/public/browser",
|
||||
"//gin",
|
||||
"//third_party/electron_node/deps/simdjson",
|
||||
"//third_party/electron_node/deps/uv",
|
||||
"//v8",
|
||||
]
|
||||
}
|
||||
|
||||
source_set("electron_lib") {
|
||||
configs += [
|
||||
"//v8:external_startup_data",
|
||||
@@ -435,6 +466,7 @@ source_set("electron_lib") {
|
||||
":electron_fuses",
|
||||
":electron_generate_node_defines",
|
||||
":electron_js2c",
|
||||
":electron_msix_updater",
|
||||
":electron_version_header",
|
||||
":resources",
|
||||
"buildflags",
|
||||
|
||||
@@ -1285,6 +1285,8 @@ For `infoType` equal to `basic`:
|
||||
|
||||
Using `basic` should be preferred if only basic information like `vendorId` or `deviceId` is needed.
|
||||
|
||||
Promise is rejected if the GPU is completely disabled, i.e. no hardware and software implementations are available.
|
||||
|
||||
### `app.setBadgeCount([count])` _Linux_ _macOS_
|
||||
|
||||
* `count` Integer (optional) - If a value is provided, set the badge to the provided value otherwise, on macOS, display a plain white dot (e.g. unknown number of notifications). On Linux, if a value is not provided the badge will not display.
|
||||
|
||||
@@ -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
|
||||
@@ -128,10 +146,12 @@ changes:
|
||||
-->
|
||||
|
||||
* `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.
|
||||
|
||||
@@ -168,3 +188,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
|
||||
|
||||
@@ -756,6 +756,9 @@ Returns [`Rectangle`](structures/rectangle.md) - The `bounds` of the window as `
|
||||
> [!NOTE]
|
||||
> On macOS, the y-coordinate value returned will be at minimum the [Tray](tray.md) height. For example, calling `win.setBounds({ x: 25, y: 20, width: 800, height: 600 })` with a tray height of 38 means that `win.getBounds()` will return `{ x: 25, y: 38, width: 800, height: 600 }`.
|
||||
|
||||
> [!NOTE]
|
||||
> On Wayland, this method will return `{ x: 0, y: 0, ... }` as introspecting or programmatically changing the global window coordinates is prohibited.
|
||||
|
||||
#### `win.getBackgroundColor()`
|
||||
|
||||
Returns `string` - Gets the background color of the window in Hex (`#RRGGBB`) format.
|
||||
@@ -969,6 +972,9 @@ Moves window to `x` and `y`.
|
||||
|
||||
Returns `Integer[]` - Contains the window's current position.
|
||||
|
||||
> [!NOTE]
|
||||
> On Wayland, this method will return `[0, 0]` as introspecting or programmatically changing the global window coordinates is prohibited.
|
||||
|
||||
#### `win.setTitle(title)`
|
||||
|
||||
* `title` string
|
||||
|
||||
@@ -862,6 +862,9 @@ Returns [`Rectangle`](structures/rectangle.md) - The `bounds` of the window as `
|
||||
> [!NOTE]
|
||||
> On macOS, the y-coordinate value returned will be at minimum the [Tray](tray.md) height. For example, calling `win.setBounds({ x: 25, y: 20, width: 800, height: 600 })` with a tray height of 38 means that `win.getBounds()` will return `{ x: 25, y: 38, width: 800, height: 600 }`.
|
||||
|
||||
> [!NOTE]
|
||||
> On Wayland, this method will return `{ x: 0, y: 0, ... }` as introspecting or programmatically changing the global window coordinates is prohibited.
|
||||
|
||||
#### `win.getBackgroundColor()`
|
||||
|
||||
Returns `string` - Gets the background color of the window in Hex (`#RRGGBB`) format.
|
||||
@@ -1087,6 +1090,9 @@ Not supported on Wayland (Linux).
|
||||
|
||||
Returns `Integer[]` - Contains the window's current position.
|
||||
|
||||
> [!NOTE]
|
||||
> On Wayland, this method will return `[0, 0]` as introspecting or programmatically changing the global window coordinates is prohibited.
|
||||
|
||||
#### `win.setTitle(title)`
|
||||
|
||||
* `title` string
|
||||
|
||||
@@ -51,7 +51,12 @@ Returns:
|
||||
* `event` Event
|
||||
* `cookie` [Cookie](structures/cookie.md) - The cookie that was changed.
|
||||
* `cause` string - The cause of the change with one of the following values:
|
||||
* `explicit` - The cookie was changed directly by a consumer's action.
|
||||
* `inserted` - The cookie was inserted.
|
||||
* `inserted-no-change-overwrite` - The newly inserted cookie overwrote a cookie but
|
||||
did not result in any change. For example, inserting an identical cookie will produce this cause.
|
||||
* `inserted-no-value-change-overwrite` - The newly inserted cookie overwrote a cookie but
|
||||
did not result in any value change, but it's web observable (e.g. updates the expiry).
|
||||
* `explicit` - The cookie was deleted directly by a consumer's action.
|
||||
* `overwrite` - The cookie was automatically removed due to an insert
|
||||
operation that overwrote it.
|
||||
* `expired` - The cookie was automatically removed as it expired.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -71,7 +71,7 @@ will disable the support for `asar` archives in Node's built-in modules.
|
||||
|
||||
### `process.noDeprecation`
|
||||
|
||||
A `boolean` that controls whether or not deprecation warnings are printed to `stderr`.
|
||||
A `boolean` (optional) that controls whether or not deprecation warnings are printed to `stderr`.
|
||||
Setting this to `true` will silence deprecation warnings. This property is used
|
||||
instead of the `--no-deprecation` command line flag.
|
||||
|
||||
@@ -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_
|
||||
|
||||
|
||||
@@ -62,9 +62,17 @@ it becomes the topmost view.
|
||||
|
||||
If the view passed as a parameter is not a child of this view, this method is a no-op.
|
||||
|
||||
#### `view.setBounds(bounds)`
|
||||
#### `view.setBounds(bounds[, options])`
|
||||
|
||||
* `bounds` [Rectangle](structures/rectangle.md) - New bounds of the View.
|
||||
* `options` Object (optional) - Options for setting the bounds.
|
||||
* `animate` boolean | Object (optional) - If true, the bounds change will be animated. If an object is passed, it can contain the following properties:
|
||||
* `duration` Integer (optional) - Duration of the animation in milliseconds. Default is `250`.
|
||||
* `easing` string (optional) - Easing function for the animation. Default is `linear`.
|
||||
* `linear`
|
||||
* `ease-in`
|
||||
* `ease-out`
|
||||
* `ease-in-out`
|
||||
|
||||
#### `view.getBounds()`
|
||||
|
||||
|
||||
@@ -30,6 +30,14 @@ Previously, PDF resources created a separate guest [WebContents](https://www.ele
|
||||
|
||||
Under the hood, Chromium [enabled](https://chromium-review.googlesource.com/c/chromium/src/+/7239572) a feature that changes PDFs to use out-of-process iframes (OOPIFs) instead of the `MimeHandlerViewGuest` extension.
|
||||
|
||||
### Behavior Changed: Updated Cookie Change Cause in the Cookie 'changed' Event
|
||||
|
||||
We have updated the [cookie](https://www.electronjs.org/docs/latest/api/cookies#event-changed) change cause in the cookie 'changed' event.
|
||||
When a new cookie is set, the change cause is `inserted`.
|
||||
When a cookie is deleted, the change cause remains `explicit`.
|
||||
When the cookie being set is identical to an existing one (same name, domain, path, and value, with no actual changes), the change cause is `inserted-no-change-overwrite`.
|
||||
When the value of the cookie being set remains unchanged but some of its attributes are updated, such as the expiration attribute, the change cause will be `inserted-no-value-change-overwrite`.
|
||||
|
||||
## Planned Breaking API Changes (40.0)
|
||||
|
||||
### Deprecated: `clipboard` API access from renderer processes
|
||||
|
||||
@@ -238,6 +238,20 @@ with 3+ years of verifiable business history and to individual developers in the
|
||||
Microsoft is looking to make the program more widely available. If you're reading this at a
|
||||
later point, it could make sense to check if the eligibility criteria have changed.
|
||||
|
||||
#### Using `jsign` for Azure Trusted Signing
|
||||
|
||||
For developers on Linux or macOS, [`jsign`](https://ebourg.github.io/jsign/) can be used to sign Windows apps via Azure Trusted Signing. Example usage:
|
||||
|
||||
```bash
|
||||
jsign --storetype TRUSTEDSIGNING \
|
||||
--keystore https://eus.codesigning.azure.net/ \
|
||||
--storepass $AZURE_ACCESS_TOKEN \
|
||||
--alias trusted-sign-acct/AppName \
|
||||
--tsaurl http://timestamp.acs.microsoft.com/ \
|
||||
--tsmode RFC3161 \
|
||||
--replace <file>
|
||||
```
|
||||
|
||||
#### Using Electron Forge
|
||||
|
||||
Electron Forge is the recommended way to sign your app as well as your `Squirrel.Windows`
|
||||
|
||||
@@ -230,8 +230,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",
|
||||
|
||||
@@ -590,6 +590,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 };
|
||||
@@ -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.5",
|
||||
"@octokit/rest": "^20.1.2",
|
||||
"@primer/octicons": "^10.0.0",
|
||||
|
||||
@@ -19,10 +19,10 @@ https://chromium-review.googlesource.com/c/chromium/src/+/6936895
|
||||
as we depend on the removed functionality in this patch.
|
||||
|
||||
diff --git a/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.mm b/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.mm
|
||||
index 7f9eab99da6300b89a7118added90f8999428a2c..3bf484c8d86022572ad32bebcb045979672227e2 100644
|
||||
index b99b8ec014c81c1d6ad14a6758568dd864102e2a..5747d9b79444674b65d481248fb0576679cdef4e 100644
|
||||
--- a/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.mm
|
||||
+++ b/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.mm
|
||||
@@ -531,7 +531,7 @@ NSUInteger CountBridgedWindows(NSArray* child_windows) {
|
||||
@@ -534,7 +534,7 @@ NSUInteger CountBridgedWindows(NSArray* child_windows) {
|
||||
is_translucent_window_ = params->is_translucent;
|
||||
pending_restoration_data_ = params->state_restoration_data;
|
||||
|
||||
|
||||
@@ -12,6 +12,9 @@ Subject: mas: avoid private macOS API usage
|
||||
* _LSSetApplicationLaunchServicesServerConnectionStatus
|
||||
* AreDeviceAndUserJoinedToDomain
|
||||
* _CFIsObjC
|
||||
* CGSSetWindowCaptureExcludeShape
|
||||
* CGRegionCreateWithRect
|
||||
* CTFontCopyVariationAxesInternal
|
||||
* AudioDeviceDuck
|
||||
* NSNextStepFrame
|
||||
* NSThemeFrame
|
||||
@@ -557,7 +560,7 @@ index 20fbdb2d4ac747aa174c5d8e19fd9f1ea48314a9..1b14feb15832bddde0ede2ac066d87a3
|
||||
if (self.isHeadless || self.parentWindow) {
|
||||
// AppKit's default implementation moves child windows down to avoid
|
||||
diff --git a/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.mm b/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.mm
|
||||
index c6cfdc7b778f2027280d8624a0d48f385365f758..7f9eab99da6300b89a7118added90f8999428a2c 100644
|
||||
index c6cfdc7b778f2027280d8624a0d48f385365f758..b99b8ec014c81c1d6ad14a6758568dd864102e2a 100644
|
||||
--- a/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.mm
|
||||
+++ b/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.mm
|
||||
@@ -42,6 +42,7 @@
|
||||
@@ -568,7 +571,21 @@ index c6cfdc7b778f2027280d8624a0d48f385365f758..7f9eab99da6300b89a7118added90f89
|
||||
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
|
||||
#include "net/cert/x509_util_apple.h"
|
||||
#include "ui/accelerated_widget_mac/window_resize_helper_mac.h"
|
||||
@@ -732,10 +733,12 @@ NSUInteger CountBridgedWindows(NSArray* child_windows) {
|
||||
@@ -74,10 +75,13 @@
|
||||
using CGRegionRef = CFTypeRef;
|
||||
|
||||
CG_EXTERN CGSConnectionID CGSMainConnectionID(void);
|
||||
+
|
||||
+#if !IS_MAS_BUILD()
|
||||
CG_EXTERN CGError CGSSetWindowCaptureExcludeShape(CGSConnectionID cid,
|
||||
CGSWindowID wid,
|
||||
CGRegionRef region);
|
||||
CG_EXTERN CGRegionRef CGRegionCreateWithRect(CGRect rect);
|
||||
+#endif
|
||||
|
||||
namespace {
|
||||
constexpr auto kUIPaintTimeout = base::Milliseconds(500);
|
||||
@@ -732,10 +736,12 @@ NSUInteger CountBridgedWindows(NSArray* child_windows) {
|
||||
// this should be treated as an error and caught early.
|
||||
CHECK(bridged_view_);
|
||||
|
||||
@@ -581,6 +598,25 @@ index c6cfdc7b778f2027280d8624a0d48f385365f758..7f9eab99da6300b89a7118added90f89
|
||||
|
||||
// Beware: This view was briefly removed (in favor of a bare CALayer) in
|
||||
// https://crrev.com/c/1236675. The ordering of unassociated layers relative
|
||||
@@ -1221,6 +1227,7 @@ NSUInteger CountBridgedWindows(NSArray* child_windows) {
|
||||
}
|
||||
|
||||
void NativeWidgetNSWindowBridge::SetAllowScreenshots(bool allow) {
|
||||
+#if !IS_MAS_BUILD()
|
||||
CGSConnectionID connection_id = CGSMainConnectionID();
|
||||
CGSWindowID window_id = ns_window().windowNumber;
|
||||
CGRect frame = ns_window().frame;
|
||||
@@ -1230,6 +1237,10 @@ NSUInteger CountBridgedWindows(NSArray* child_windows) {
|
||||
region.reset(CGRegionCreateWithRect(frame));
|
||||
}
|
||||
CGSSetWindowCaptureExcludeShape(connection_id, window_id, region.get());
|
||||
+#else
|
||||
+ [ns_window()
|
||||
+ setSharingType:allow ? NSWindowSharingReadOnly : NSWindowSharingNone];
|
||||
+#endif
|
||||
}
|
||||
|
||||
void NativeWidgetNSWindowBridge::SetColorMode(
|
||||
diff --git a/components/viz/service/BUILD.gn b/components/viz/service/BUILD.gn
|
||||
index 10497f25d8386ae2807d0a3389ba303f75ae1b20..6ed74ce305052e3f88727d15277aa86eafdffcd0 100644
|
||||
--- a/components/viz/service/BUILD.gn
|
||||
@@ -1602,6 +1638,46 @@ index 94afefcee81b87c05bf9b1199d90d3d4b5ea84a6..3e3aaea0ec6c8fad0d90a931d269af3a
|
||||
}
|
||||
|
||||
} // namespace blink
|
||||
diff --git a/third_party/blink/renderer/platform/fonts/mac/font_matcher_mac.mm b/third_party/blink/renderer/platform/fonts/mac/font_matcher_mac.mm
|
||||
index a4cc39ba2796d3108ff6674e60dcf5302c1da7d7..3aa68d45e2c17fe52b6c6cc5527af7dd42ad0c3a 100644
|
||||
--- a/third_party/blink/renderer/platform/fonts/mac/font_matcher_mac.mm
|
||||
+++ b/third_party/blink/renderer/platform/fonts/mac/font_matcher_mac.mm
|
||||
@@ -37,6 +37,7 @@
|
||||
#include "base/apple/bridging.h"
|
||||
#include "base/apple/foundation_util.h"
|
||||
#include "base/apple/scoped_cftyperef.h"
|
||||
+#include "electron/mas.h"
|
||||
#include "third_party/blink/renderer/platform/fonts/font_cache.h"
|
||||
#include "third_party/blink/renderer/platform/fonts/font_selection_types.h"
|
||||
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
|
||||
@@ -60,8 +61,10 @@
|
||||
// We don't need localized variation axis name, so we can use
|
||||
// `CTFontCopyVariationAxesInternal()` instead.
|
||||
// Request for public API: FB13788219.
|
||||
+#if !IS_MAS_BUILD()
|
||||
extern "C" CFArrayRef CTFontCopyVariationAxesInternal(CTFontRef)
|
||||
CT_AVAILABLE(macos(12.1));
|
||||
+#endif
|
||||
|
||||
namespace blink {
|
||||
|
||||
@@ -403,12 +406,16 @@ void ClampVariationValuesToFontAcceptableRange(
|
||||
// we are enabling it only on MacOS 13+ because these are our benchmarking
|
||||
// platforms.
|
||||
NSArray* all_axes;
|
||||
+#if !IS_MAS_BUILD()
|
||||
if (@available(macOS 13.0, *)) {
|
||||
all_axes =
|
||||
CFToNSOwnershipCast(CTFontCopyVariationAxesInternal(ct_font.get()));
|
||||
} else {
|
||||
all_axes = CFToNSOwnershipCast(CTFontCopyVariationAxes(ct_font.get()));
|
||||
}
|
||||
+#else
|
||||
+ all_axes = CFToNSOwnershipCast(CTFontCopyVariationAxes(ct_font.get()));
|
||||
+#endif
|
||||
if (!all_axes) {
|
||||
return;
|
||||
}
|
||||
diff --git a/ui/accelerated_widget_mac/BUILD.gn b/ui/accelerated_widget_mac/BUILD.gn
|
||||
index 0f8a6f75b7f01029adc2f5fd23559bacce19cf72..cf66c2f4f02a8e21cc83c3b7389fc5156bcd93ba 100644
|
||||
--- a/ui/accelerated_widget_mac/BUILD.gn
|
||||
|
||||
@@ -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];
|
||||
@@ -8,24 +8,10 @@
|
||||
"parallel/test-cluster-primary-error",
|
||||
"parallel/test-cluster-primary-kill",
|
||||
"parallel/test-config-file",
|
||||
"parallel/test-crypto-aes-wrap",
|
||||
"parallel/test-crypto-authenticated",
|
||||
"parallel/test-crypto-authenticated-stream",
|
||||
"parallel/test-crypto-default-shake-lengths",
|
||||
"parallel/test-crypto-des3-wrap",
|
||||
"parallel/test-crypto-dh-group-setters",
|
||||
"parallel/test-crypto-dh-modp2",
|
||||
"parallel/test-crypto-dh-modp2-views",
|
||||
"parallel/test-crypto-dh-stateless",
|
||||
"parallel/test-crypto-ecb",
|
||||
"parallel/test-crypto-fips",
|
||||
"parallel/test-crypto-key-objects",
|
||||
"parallel/test-crypto-keygen",
|
||||
"parallel/test-crypto-keygen-deprecation",
|
||||
"parallel/test-crypto-rsa-dsa",
|
||||
"parallel/test-crypto-padding-aes256",
|
||||
"parallel/test-crypto-secure-heap",
|
||||
"parallel/test-dgram-send-cb-quelches-error",
|
||||
"parallel/test-domain-error-types",
|
||||
"parallel/test-fs-utimes-y2K38",
|
||||
"parallel/test-http2-clean-output",
|
||||
@@ -44,34 +30,10 @@
|
||||
"parallel/test-os-checked-function",
|
||||
"parallel/test-process-versions",
|
||||
"parallel/test-process-get-builtin",
|
||||
"parallel/test-repl",
|
||||
"parallel/test-repl-mode",
|
||||
"parallel/test-repl-underscore",
|
||||
"parallel/test-shadow-realm-custom-loaders",
|
||||
"parallel/test-snapshot-api",
|
||||
"parallel/test-snapshot-argv1",
|
||||
"parallel/test-snapshot-basic",
|
||||
"parallel/test-snapshot-cjs-main",
|
||||
"parallel/test-snapshot-cwd",
|
||||
"parallel/test-snapshot-console",
|
||||
"parallel/test-snapshot-config",
|
||||
"parallel/test-snapshot-dns-lookup-localhost",
|
||||
"parallel/test-snapshot-dns-lookup-localhost-promise",
|
||||
"parallel/test-snapshot-dns-resolve-localhost",
|
||||
"parallel/test-snapshot-dns-resolve-localhost-promise",
|
||||
"parallel/test-snapshot-error",
|
||||
"parallel/test-snapshot-eval",
|
||||
"parallel/test-snapshot-gzip",
|
||||
"parallel/test-snapshot-incompatible",
|
||||
"parallel/test-snapshot-namespaced-builtin",
|
||||
"parallel/test-snapshot-net",
|
||||
"parallel/test-snapshot-reproducible",
|
||||
"parallel/test-snapshot-stack-trace-limit",
|
||||
"parallel/test-snapshot-stack-trace-limit-mutation",
|
||||
"parallel/test-snapshot-typescript",
|
||||
"parallel/test-snapshot-umd",
|
||||
"parallel/test-snapshot-warning",
|
||||
"parallel/test-snapshot-weak-reference",
|
||||
"parallel/test-snapshot-worker",
|
||||
"parallel/test-snapshot",
|
||||
"parallel/test-strace-openat-openssl",
|
||||
"parallel/test-sqlite-backup",
|
||||
"parallel/test-max-old-space-size-percentage",
|
||||
@@ -107,15 +69,12 @@
|
||||
"parallel/test-tls-multi-key",
|
||||
"parallel/test-tls-multi-pfx",
|
||||
"parallel/test-tls-no-cert-required",
|
||||
"parallel/test-tls-no-sslv23.js",
|
||||
"parallel/test-tls-no-sslv23",
|
||||
"parallel/test-tls-options-boolean-check",
|
||||
"parallel/test-tls-passphrase",
|
||||
"parallel/test-tls-peer-certificate",
|
||||
"parallel/test-tls-pfx-authorizationerror",
|
||||
"parallel/test-tls-psk-alpn-callback-exception-handling",
|
||||
"parallel/test-tls-psk-circuit",
|
||||
"parallel/test-tls-reduced-SECLEVEL-in-cipher",
|
||||
"parallel/test-tls-root-certificates",
|
||||
"parallel/test-tls-server-failed-handshake-emits-clienterror",
|
||||
"parallel/test-tls-set-ciphers",
|
||||
"parallel/test-tls-set-ciphers-error",
|
||||
@@ -125,45 +84,13 @@
|
||||
"parallel/test-tls-socket-failed-handshake-emits-error",
|
||||
"parallel/test-tls-ticket",
|
||||
"parallel/test-tls-ticket-cluster",
|
||||
"parallel/test-trace-events-all",
|
||||
"parallel/test-trace-events-async-hooks",
|
||||
"parallel/test-trace-events-binding",
|
||||
"parallel/test-trace-events-bootstrap",
|
||||
"parallel/test-trace-events-category-used",
|
||||
"parallel/test-trace-events-console",
|
||||
"parallel/test-trace-events-dynamic-enable",
|
||||
"parallel/test-trace-events-dynamic-enable-workers-disabled",
|
||||
"parallel/test-trace-events-environment",
|
||||
"parallel/test-trace-events-file-pattern",
|
||||
"parallel/test-trace-events-fs-async",
|
||||
"parallel/test-trace-events-fs-sync",
|
||||
"parallel/test-trace-events-http",
|
||||
"parallel/test-trace-events-metadata",
|
||||
"parallel/test-trace-events-net",
|
||||
"parallel/test-trace-events-none",
|
||||
"parallel/test-trace-events-process-exit",
|
||||
"parallel/test-trace-events-promises",
|
||||
"parallel/test-trace-events-threadpool",
|
||||
"parallel/test-trace-events-v8",
|
||||
"parallel/test-trace-events-vm",
|
||||
"parallel/test-trace-events-worker-metadata",
|
||||
"parallel/test-trace-events",
|
||||
"parallel/test-tz-version",
|
||||
"parallel/test-webcrypto-derivebits-cfrg",
|
||||
"parallel/test-webcrypto-derivekey-cfrg",
|
||||
"parallel/test-webcrypto-encrypt-decrypt",
|
||||
"parallel/test-webcrypto-encrypt-decrypt-aes",
|
||||
"parallel/test-webcrypto-encrypt-decrypt-rsa",
|
||||
"parallel/test-webcrypto-export-import-cfrg",
|
||||
"parallel/test-webcrypto-keygen",
|
||||
"parallel/test-webcrypto-sign-verify-eddsa",
|
||||
"parallel/test-webcrypto-wrap-unwrap",
|
||||
"parallel/test-worker-no-sab",
|
||||
"parallel/test-worker-resource-limits",
|
||||
"parallel/test-zlib-unused-weak",
|
||||
"report/test-report-fatalerror-oomerror-compact",
|
||||
"report/test-report-fatalerror-oomerror-directory",
|
||||
"report/test-report-fatalerror-oomerror-filename",
|
||||
"report/test-report-fatalerror-oomerror-set",
|
||||
"report/test-report-fatalerror-oomerror",
|
||||
"report/test-report-getreport",
|
||||
"report/test-report-signal",
|
||||
"report/test-report-uncaught-exception",
|
||||
@@ -173,12 +100,6 @@
|
||||
"report/test-report-writereport",
|
||||
"sea/test-single-executable-blob-config",
|
||||
"sea/test-single-executable-blob-config-errors",
|
||||
"sequential/test-single-executable-application",
|
||||
"sequential/test-single-executable-application-disable-experimental-sea-warning",
|
||||
"sequential/test-single-executable-application-empty",
|
||||
"sequential/test-single-executable-application-snapshot",
|
||||
"sequential/test-single-executable-application-snapshot-and-code-cache",
|
||||
"sequential/test-single-executable-application-use-code-cache",
|
||||
"sequential/test-tls-connect",
|
||||
"wpt/test-webcrypto",
|
||||
"wasm-allocation/test-wasm-allocation"
|
||||
|
||||
@@ -118,7 +118,7 @@ async function runClangTidy (
|
||||
fix: boolean = false
|
||||
): Promise<boolean> {
|
||||
const cmd = path.resolve(LLVM_BIN, 'clang-tidy');
|
||||
const args = [`-p=${outDir}`];
|
||||
const args = [`-p=${outDir}`, "-header-filter=''"];
|
||||
|
||||
if (!process.env.CI) args.push('--use-color');
|
||||
if (fix) args.push('--fix');
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -80,6 +80,11 @@ struct Converter<net::CookieChangeCause> {
|
||||
const net::CookieChangeCause& val) {
|
||||
switch (val) {
|
||||
case net::CookieChangeCause::INSERTED:
|
||||
return gin::StringToV8(isolate, "inserted");
|
||||
case net::CookieChangeCause::INSERTED_NO_CHANGE_OVERWRITE:
|
||||
return gin::StringToV8(isolate, "inserted-no-change-overwrite");
|
||||
case net::CookieChangeCause::INSERTED_NO_VALUE_CHANGE_OVERWRITE:
|
||||
return gin::StringToV8(isolate, "inserted-no-value-change-overwrite");
|
||||
case net::CookieChangeCause::EXPLICIT:
|
||||
return gin::StringToV8(isolate, "explicit");
|
||||
case net::CookieChangeCause::OVERWRITE:
|
||||
@@ -274,6 +279,17 @@ std::string StringToCookieSameSite(const std::string* str_ptr,
|
||||
return "";
|
||||
}
|
||||
|
||||
bool IsDeletion(net::CookieChangeCause cause) {
|
||||
switch (cause) {
|
||||
case net::CookieChangeCause::INSERTED:
|
||||
case net::CookieChangeCause::INSERTED_NO_CHANGE_OVERWRITE:
|
||||
case net::CookieChangeCause::INSERTED_NO_VALUE_CHANGE_OVERWRITE:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
gin::DeprecatedWrapperInfo Cookies::kWrapperInfo = {gin::kEmbedderNativeGin};
|
||||
@@ -437,10 +453,10 @@ v8::Local<v8::Promise> Cookies::FlushStore(v8::Isolate* isolate) {
|
||||
void Cookies::OnCookieChanged(const net::CookieChangeInfo& change) {
|
||||
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
|
||||
v8::HandleScope scope(isolate);
|
||||
bool is_deletion = IsDeletion(change.cause);
|
||||
Emit("changed", gin::ConvertToV8(isolate, change.cookie),
|
||||
gin::ConvertToV8(isolate, change.cause),
|
||||
gin::ConvertToV8(isolate,
|
||||
change.cause != net::CookieChangeCause::INSERTED));
|
||||
gin::ConvertToV8(isolate, is_deletion));
|
||||
}
|
||||
|
||||
// static
|
||||
|
||||
@@ -68,6 +68,10 @@ Menu::Menu(gin::Arguments* args)
|
||||
}
|
||||
|
||||
Menu::~Menu() {
|
||||
RemoveModelObserver();
|
||||
}
|
||||
|
||||
void Menu::RemoveModelObserver() {
|
||||
if (model_) {
|
||||
model_->RemoveObserver(this);
|
||||
}
|
||||
|
||||
@@ -61,6 +61,10 @@ class Menu : public gin::Wrappable<Menu>,
|
||||
ElectronMenuModel* model() const { return model_.get(); }
|
||||
|
||||
protected:
|
||||
// Remove this instance as an observer from the model. Called by derived
|
||||
// class destructors to ensure observer is removed before platform-specific
|
||||
// cleanup that may trigger model callbacks.
|
||||
void RemoveModelObserver();
|
||||
// Returns a new callback which keeps references of the JS wrapper until the
|
||||
// passed |callback| is called.
|
||||
base::OnceClosure BindSelfToClosure(base::OnceClosure callback);
|
||||
|
||||
@@ -51,7 +51,11 @@ namespace electron::api {
|
||||
|
||||
MenuMac::MenuMac(gin::Arguments* args) : Menu{args} {}
|
||||
|
||||
MenuMac::~MenuMac() = default;
|
||||
MenuMac::~MenuMac() {
|
||||
// Must remove observer before destroying menu_controller_, which holds
|
||||
// a weak reference to model_
|
||||
RemoveModelObserver();
|
||||
}
|
||||
|
||||
void MenuMac::PopupAt(BaseWindow* window,
|
||||
std::optional<WebFrameMain*> frame,
|
||||
|
||||
528
shell/browser/api/electron_api_msix_updater.cc
Normal file
528
shell/browser/api/electron_api_msix_updater.cc
Normal file
@@ -0,0 +1,528 @@
|
||||
// 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 "base/task/task_traits.h"
|
||||
#include "base/task/thread_pool.h"
|
||||
#include "content/public/browser/browser_task_traits.h"
|
||||
#include "content/public/browser/browser_thread.h"
|
||||
#include "shell/browser/browser.h"
|
||||
#include "shell/browser/javascript_environment.h"
|
||||
#include "shell/browser/native_window.h"
|
||||
#include "shell/browser/window_list.h"
|
||||
#include "shell/common/gin_helper/dictionary.h"
|
||||
#include "shell/common/gin_helper/error_thrower.h"
|
||||
#include "shell/common/gin_helper/promise.h"
|
||||
#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>
|
||||
// Use pre-generated C++/WinRT headers from //third_party/nearby instead of the
|
||||
// SDK's cppwinrt headers, which are missing implementation files.
|
||||
#include "third_party/nearby/src/internal/platform/implementation/windows/generated/winrt/Windows.ApplicationModel.h"
|
||||
#include "third_party/nearby/src/internal/platform/implementation/windows/generated/winrt/Windows.Foundation.Collections.h"
|
||||
#include "third_party/nearby/src/internal/platform/implementation/windows/generated/winrt/Windows.Foundation.Metadata.h"
|
||||
#include "third_party/nearby/src/internal/platform/implementation/windows/generated/winrt/Windows.Foundation.h"
|
||||
#include "third_party/nearby/src/internal/platform/implementation/windows/generated/winrt/Windows.Management.Deployment.h"
|
||||
#include "third_party/nearby/src/internal/platform/implementation/windows/generated/winrt/base.h"
|
||||
|
||||
#include "base/win/scoped_com_initializer.h"
|
||||
#endif
|
||||
|
||||
namespace electron {
|
||||
|
||||
const bool debug_msix_updater =
|
||||
base::Environment::Create()->HasVar("ELECTRON_DEBUG_MSIX_UPDATER");
|
||||
|
||||
} // namespace electron
|
||||
|
||||
namespace {
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
// 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;
|
||||
};
|
||||
|
||||
// Performs MSIX update on IO thread
|
||||
void DoUpdateMsix(const std::string& package_uri,
|
||||
UpdateMsixOptions opts,
|
||||
scoped_refptr<base::SingleThreadTaskRunner> reply_runner,
|
||||
gin_helper::Promise<void> promise) {
|
||||
DebugLog("DoUpdateMsix: Starting");
|
||||
|
||||
using winrt::Windows::Foundation::AsyncStatus;
|
||||
using winrt::Windows::Foundation::Uri;
|
||||
using winrt::Windows::Management::Deployment::AddPackageOptions;
|
||||
using winrt::Windows::Management::Deployment::DeploymentResult;
|
||||
using winrt::Windows::Management::Deployment::PackageManager;
|
||||
|
||||
std::string error;
|
||||
std::wstring packageUriString =
|
||||
std::wstring(package_uri.begin(), package_uri.end());
|
||||
Uri uri{packageUriString};
|
||||
PackageManager packageManager;
|
||||
AddPackageOptions packageOptions;
|
||||
|
||||
// Use the pre-parsed options
|
||||
packageOptions.DeferRegistrationWhenPackagesAreInUse(opts.defer_registration);
|
||||
packageOptions.DeveloperMode(opts.developer_mode);
|
||||
packageOptions.ForceAppShutdown(opts.force_shutdown);
|
||||
packageOptions.ForceTargetAppShutdown(opts.force_target_shutdown);
|
||||
packageOptions.ForceUpdateFromAnyVersion(opts.force_update_from_any_version);
|
||||
|
||||
{
|
||||
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());
|
||||
}
|
||||
|
||||
auto deploymentOperation =
|
||||
packageManager.AddPackageByUriAsync(uri, packageOptions);
|
||||
|
||||
if (!deploymentOperation) {
|
||||
DebugLog("Deployment operation is null");
|
||||
error =
|
||||
"Deployment is NULL. See "
|
||||
"http://go.microsoft.com/fwlink/?LinkId=235160 for diagnosing.";
|
||||
} else {
|
||||
if (!opts.force_shutdown && !opts.force_target_shutdown) {
|
||||
DebugLog("Waiting for deployment...");
|
||||
deploymentOperation.get();
|
||||
DebugLog("Deployment finished.");
|
||||
|
||||
if (deploymentOperation.Status() == AsyncStatus::Error) {
|
||||
auto deploymentResult{deploymentOperation.GetResults()};
|
||||
std::string errorText = winrt::to_string(deploymentResult.ErrorText());
|
||||
std::string errorCode =
|
||||
std::to_string(static_cast<int>(deploymentOperation.ErrorCode()));
|
||||
error = errorText + " (" + errorCode + ")";
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "Deployment failed: " << error;
|
||||
DebugLog(oss.str());
|
||||
}
|
||||
} else if (deploymentOperation.Status() == AsyncStatus::Canceled) {
|
||||
DebugLog("Deployment canceled");
|
||||
error = "Deployment canceled";
|
||||
} else if (deploymentOperation.Status() == AsyncStatus::Completed) {
|
||||
DebugLog("MSIX Deployment completed.");
|
||||
} else {
|
||||
error = "Deployment status unknown";
|
||||
DebugLog("Deployment status unknown");
|
||||
}
|
||||
} else {
|
||||
// At this point, we can not await the deployment because we require a
|
||||
// shutdown of the app to continue, so we do a fire and forget. When the
|
||||
// deployment process tries ot shutdown the app, the process waits for us
|
||||
// to finish here. But to finish we need to shutdow. That leads to a 30s
|
||||
// dealock, till we forcefully get shutdown by the OS.
|
||||
DebugLog(
|
||||
"Deployment initiated. Force shutdown or target shutdown requested. "
|
||||
"Good bye!");
|
||||
}
|
||||
}
|
||||
|
||||
// Post result back
|
||||
reply_runner->PostTask(
|
||||
FROM_HERE, base::BindOnce(
|
||||
[](gin_helper::Promise<void> promise, std::string error) {
|
||||
if (error.empty()) {
|
||||
promise.Resolve();
|
||||
} else {
|
||||
promise.RejectWithErrorMessage(error);
|
||||
}
|
||||
},
|
||||
std::move(promise), error));
|
||||
}
|
||||
|
||||
// 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");
|
||||
|
||||
using winrt::Windows::Foundation::AsyncStatus;
|
||||
using winrt::Windows::Foundation::Collections::IIterable;
|
||||
using winrt::Windows::Management::Deployment::DeploymentOptions;
|
||||
using winrt::Windows::Management::Deployment::PackageManager;
|
||||
|
||||
std::string error;
|
||||
auto familyNameH = winrt::to_hstring(family_name);
|
||||
PackageManager packageManager;
|
||||
DeploymentOptions deploymentOptions = DeploymentOptions::None;
|
||||
|
||||
// Use the pre-parsed options (no V8 access needed)
|
||||
if (opts.force_shutdown) {
|
||||
deploymentOptions |= DeploymentOptions::ForceApplicationShutdown;
|
||||
}
|
||||
if (opts.force_target_shutdown) {
|
||||
deploymentOptions |= DeploymentOptions::ForceTargetApplicationShutdown;
|
||||
}
|
||||
if (opts.force_update_from_any_version) {
|
||||
deploymentOptions |= DeploymentOptions::ForceUpdateFromAnyVersion;
|
||||
}
|
||||
|
||||
// Create empty collections for dependency and optional packages
|
||||
IIterable<winrt::hstring> emptyDependencies{nullptr};
|
||||
IIterable<winrt::hstring> emptyOptional{nullptr};
|
||||
|
||||
{
|
||||
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());
|
||||
}
|
||||
|
||||
auto deploymentOperation = packageManager.RegisterPackageByFamilyNameAsync(
|
||||
familyNameH, emptyDependencies, deploymentOptions, nullptr,
|
||||
emptyOptional);
|
||||
|
||||
if (!deploymentOperation) {
|
||||
error =
|
||||
"Deployment is NULL. See "
|
||||
"http://go.microsoft.com/fwlink/?LinkId=235160 for diagnosing.";
|
||||
} else {
|
||||
if (!opts.force_shutdown && !opts.force_target_shutdown) {
|
||||
DebugLog("Waiting for registration...");
|
||||
deploymentOperation.get();
|
||||
DebugLog("Registration finished.");
|
||||
|
||||
if (deploymentOperation.Status() == AsyncStatus::Error) {
|
||||
auto deploymentResult{deploymentOperation.GetResults()};
|
||||
std::string errorText = winrt::to_string(deploymentResult.ErrorText());
|
||||
std::string errorCode =
|
||||
std::to_string(static_cast<int>(deploymentOperation.ErrorCode()));
|
||||
error = errorText + " (" + errorCode + ")";
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "Registration failed: " << error;
|
||||
DebugLog(oss.str());
|
||||
}
|
||||
} else if (deploymentOperation.Status() == AsyncStatus::Canceled) {
|
||||
DebugLog("Registration canceled");
|
||||
error = "Registration canceled";
|
||||
} else if (deploymentOperation.Status() == AsyncStatus::Completed) {
|
||||
DebugLog("MSIX Registration completed.");
|
||||
} else {
|
||||
error = "Registration status unknown";
|
||||
DebugLog("Registration status unknown");
|
||||
}
|
||||
} else {
|
||||
// At this point, we can not await the registration because we require a
|
||||
// shutdown of the app to continue, so we do a fire and forget. When the
|
||||
// registration process tries ot shutdown the app, the process waits for
|
||||
// us to finish here. But to finish we need to shutdown. That leads to a
|
||||
// 30s dealock, till we forcefully get shutdown by the OS.
|
||||
DebugLog(
|
||||
"Registration initiated. Force shutdown or target shutdown "
|
||||
"requested. Good bye!");
|
||||
}
|
||||
}
|
||||
|
||||
// Post result back to UI thread
|
||||
reply_runner->PostTask(
|
||||
FROM_HERE, base::BindOnce(
|
||||
[](gin_helper::Promise<void> promise, std::string error) {
|
||||
if (error.empty()) {
|
||||
promise.Resolve();
|
||||
} else {
|
||||
promise.RejectWithErrorMessage(error);
|
||||
}
|
||||
},
|
||||
std::move(promise), error));
|
||||
}
|
||||
#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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
const wchar_t* commandLine = nullptr;
|
||||
// Flags: RESTART_NO_CRASH | RESTART_NO_HANG | RESTART_NO_REBOOT
|
||||
// This means: only restart on updates (RESTART_NO_PATCH is NOT set)
|
||||
const DWORD dwFlags = 1 | 2 | 8; // 11
|
||||
|
||||
if (!command_line.empty()) {
|
||||
std::wstring commandLineW =
|
||||
std::wstring(command_line.begin(), command_line.end());
|
||||
commandLine = commandLineW.c_str();
|
||||
}
|
||||
|
||||
HRESULT hr = RegisterApplicationRestart(commandLine, 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)
|
||||
if (winrt::Windows::Foundation::Metadata::ApiInformation::
|
||||
IsApiContractPresent(L"Windows.Foundation.UniversalApiContract", 7)) {
|
||||
using winrt::Windows::ApplicationModel::Package;
|
||||
using winrt::Windows::ApplicationModel::PackageSignatureKind;
|
||||
Package package = Package::Current();
|
||||
|
||||
// Get package ID and family name
|
||||
std::string packageId = winrt::to_string(package.Id().FullName());
|
||||
std::string familyName = winrt::to_string(package.Id().FamilyName());
|
||||
|
||||
result.Set("id", packageId);
|
||||
result.Set("familyName", familyName);
|
||||
result.Set("developmentMode", package.IsDevelopmentMode());
|
||||
|
||||
// Get package version
|
||||
auto packageVersion = package.Id().Version();
|
||||
std::string version = std::to_string(packageVersion.Major) + "." +
|
||||
std::to_string(packageVersion.Minor) + "." +
|
||||
std::to_string(packageVersion.Build) + "." +
|
||||
std::to_string(packageVersion.Revision);
|
||||
result.Set("version", version);
|
||||
|
||||
// Convert signature kind to string
|
||||
std::string signatureKind;
|
||||
switch (package.SignatureKind()) {
|
||||
case PackageSignatureKind::Developer:
|
||||
signatureKind = "developer";
|
||||
break;
|
||||
case PackageSignatureKind::Enterprise:
|
||||
signatureKind = "enterprise";
|
||||
break;
|
||||
case PackageSignatureKind::None:
|
||||
signatureKind = "none";
|
||||
break;
|
||||
case PackageSignatureKind::Store:
|
||||
signatureKind = "store";
|
||||
break;
|
||||
case PackageSignatureKind::System:
|
||||
signatureKind = "system";
|
||||
break;
|
||||
default:
|
||||
signatureKind = "none";
|
||||
break;
|
||||
}
|
||||
result.Set("signatureKind", signatureKind);
|
||||
|
||||
// Get app installer info if available
|
||||
auto appInstallerInfo = package.GetAppInstallerInfo();
|
||||
if (appInstallerInfo != nullptr) {
|
||||
std::string uriStr = winrt::to_string(appInstallerInfo.Uri().ToString());
|
||||
result.Set("appInstallerUri", uriStr);
|
||||
}
|
||||
} 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_
|
||||
@@ -70,16 +70,26 @@ void DelayEmitWithMetrics(Screen* screen,
|
||||
screen->Emit(name, display, metrics);
|
||||
}
|
||||
|
||||
// Calls the one-liner `display::Screen::Get()` to get ui's global screen.
|
||||
// NOTE: during shutdown, that screen can be destroyed before us. This means:
|
||||
// 1. Call this instead of keeping a possibly-dangling raw_ptr in api::Screen.
|
||||
// 2. Always check this function's return value for nullptr before use.
|
||||
[[nodiscard]] auto* GetDisplayScreen() {
|
||||
return display::Screen::Get();
|
||||
}
|
||||
|
||||
[[nodiscard]] auto GetFallbackDisplay() {
|
||||
return display::Display::GetDefaultDisplay();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Screen::Screen(display::Screen* screen) : screen_{screen} {
|
||||
screen_->AddObserver(this);
|
||||
Screen::Screen() {
|
||||
if (auto* screen = GetDisplayScreen())
|
||||
screen->AddObserver(this);
|
||||
}
|
||||
|
||||
Screen::~Screen() {
|
||||
// Use `display::Screen::Get()` here, not our cached `screen_`:
|
||||
// during shutdown, it can get torn down before us.
|
||||
if (auto* screen = display::Screen::Get())
|
||||
if (auto* screen = GetDisplayScreen())
|
||||
screen->RemoveObserver(this);
|
||||
}
|
||||
|
||||
@@ -95,7 +105,33 @@ gfx::Point Screen::GetCursorScreenPoint(v8::Isolate* isolate) {
|
||||
return {};
|
||||
}
|
||||
#endif
|
||||
return screen_->GetCursorScreenPoint();
|
||||
auto* screen = GetDisplayScreen();
|
||||
return screen ? screen->GetCursorScreenPoint() : gfx::Point{};
|
||||
}
|
||||
|
||||
display::Display Screen::GetPrimaryDisplay() const {
|
||||
const auto* screen = GetDisplayScreen();
|
||||
return screen ? screen->GetPrimaryDisplay() : GetFallbackDisplay();
|
||||
}
|
||||
|
||||
std::vector<display::Display> Screen::GetAllDisplays() const {
|
||||
if (const auto* screen = GetDisplayScreen())
|
||||
return screen->GetAllDisplays();
|
||||
|
||||
// Even though this is only reached during shutdown by Screen::Get() failing,
|
||||
// display::Screen::GetAllDisplays() is guaranteed to return >= 1 display.
|
||||
// For consistency with that API, let's return a nonempty vector here.
|
||||
return {GetFallbackDisplay()};
|
||||
}
|
||||
|
||||
display::Display Screen::GetDisplayNearestPoint(const gfx::Point& point) const {
|
||||
const auto* screen = GetDisplayScreen();
|
||||
return screen ? screen->GetDisplayNearestPoint(point) : GetFallbackDisplay();
|
||||
}
|
||||
|
||||
display::Display Screen::GetDisplayMatching(const gfx::Rect& match_rect) const {
|
||||
const auto* screen = GetDisplayScreen();
|
||||
return screen ? screen->GetDisplayMatching(match_rect) : GetFallbackDisplay();
|
||||
}
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
@@ -182,14 +218,14 @@ Screen* Screen::Create(gin_helper::ErrorThrower error_thrower) {
|
||||
return {};
|
||||
}
|
||||
|
||||
display::Screen* screen = display::Screen::Get();
|
||||
display::Screen* screen = GetDisplayScreen();
|
||||
if (!screen) {
|
||||
error_thrower.ThrowError("Failed to get screen information");
|
||||
return {};
|
||||
}
|
||||
|
||||
return cppgc::MakeGarbageCollected<Screen>(
|
||||
error_thrower.isolate()->GetCppHeap()->GetAllocationHandle(), screen);
|
||||
error_thrower.isolate()->GetCppHeap()->GetAllocationHandle());
|
||||
}
|
||||
|
||||
gin::ObjectTemplateBuilder Screen::GetObjectTemplateBuilder(
|
||||
@@ -218,7 +254,6 @@ const gin::WrapperInfo* Screen::wrapper_info() const {
|
||||
const char* Screen::GetHumanReadableName() const {
|
||||
return "Electron / Screen";
|
||||
}
|
||||
|
||||
} // namespace electron::api
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "base/memory/raw_ptr.h"
|
||||
#include "gin/wrappable.h"
|
||||
#include "shell/browser/event_emitter_mixin.h"
|
||||
#include "ui/display/display_observer.h"
|
||||
@@ -43,22 +42,16 @@ class Screen final : public gin::Wrappable<Screen>,
|
||||
Screen& operator=(const Screen&) = delete;
|
||||
|
||||
// Make public for cppgc::MakeGarbageCollected.
|
||||
explicit Screen(display::Screen* screen);
|
||||
Screen();
|
||||
~Screen() override;
|
||||
|
||||
gfx::Point GetCursorScreenPoint(v8::Isolate* isolate);
|
||||
display::Display GetPrimaryDisplay() const {
|
||||
return screen_->GetPrimaryDisplay();
|
||||
}
|
||||
const std::vector<display::Display>& GetAllDisplays() const {
|
||||
return screen_->GetAllDisplays();
|
||||
}
|
||||
display::Display GetDisplayNearestPoint(const gfx::Point& point) const {
|
||||
return screen_->GetDisplayNearestPoint(point);
|
||||
}
|
||||
display::Display GetDisplayMatching(const gfx::Rect& match_rect) const {
|
||||
return screen_->GetDisplayMatching(match_rect);
|
||||
}
|
||||
[[nodiscard]] gfx::Point GetCursorScreenPoint(v8::Isolate* isolate);
|
||||
[[nodiscard]] display::Display GetPrimaryDisplay() const;
|
||||
[[nodiscard]] std::vector<display::Display> GetAllDisplays() const;
|
||||
[[nodiscard]] display::Display GetDisplayNearestPoint(
|
||||
const gfx::Point& point) const;
|
||||
[[nodiscard]] display::Display GetDisplayMatching(
|
||||
const gfx::Rect& match_rect) const;
|
||||
|
||||
gfx::PointF ScreenToDIPPoint(const gfx::PointF& point_px);
|
||||
gfx::Point DIPToScreenPoint(const gfx::Point& point_dip);
|
||||
@@ -68,9 +61,6 @@ class Screen final : public gin::Wrappable<Screen>,
|
||||
void OnDisplaysRemoved(const display::Displays& removed_displays) override;
|
||||
void OnDisplayMetricsChanged(const display::Display& display,
|
||||
uint32_t changed_metrics) override;
|
||||
|
||||
private:
|
||||
raw_ptr<display::Screen> screen_;
|
||||
};
|
||||
|
||||
} // namespace electron::api
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
#include "shell/common/gin_helper/handle.h"
|
||||
#include "shell/common/gin_helper/object_template_builder.h"
|
||||
#include "shell/common/node_includes.h"
|
||||
#include "ui/compositor/layer.h"
|
||||
#include "ui/views/animation/animation_builder.h"
|
||||
#include "ui/views/background.h"
|
||||
#include "ui/views/layout/flex_layout.h"
|
||||
#include "ui/views/layout/layout_manager_base.h"
|
||||
@@ -144,6 +146,27 @@ struct Converter<views::SizeBounds> {
|
||||
.Build();
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct Converter<gfx::Tween::Type> {
|
||||
static bool FromV8(v8::Isolate* isolate,
|
||||
v8::Local<v8::Value> val,
|
||||
gfx::Tween::Type* out) {
|
||||
std::string easing = base::ToLowerASCII(gin::V8ToString(isolate, val));
|
||||
if (easing == "linear") {
|
||||
*out = gfx::Tween::LINEAR;
|
||||
} else if (easing == "ease-in") {
|
||||
*out = gfx::Tween::EASE_IN;
|
||||
} else if (easing == "ease-out") {
|
||||
*out = gfx::Tween::EASE_OUT;
|
||||
} else if (easing == "ease-in-out") {
|
||||
*out = gfx::Tween::EASE_IN_OUT;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
} // namespace gin
|
||||
|
||||
namespace electron::api {
|
||||
@@ -280,10 +303,116 @@ void View::RemoveChildView(gin_helper::Handle<View> child) {
|
||||
}
|
||||
}
|
||||
|
||||
void View::SetBounds(const gfx::Rect& bounds) {
|
||||
ui::Layer* View::GetLayer() {
|
||||
if (!view_)
|
||||
return nullptr;
|
||||
|
||||
if (view_->layer())
|
||||
return view_->layer();
|
||||
|
||||
view_->SetPaintToLayer();
|
||||
|
||||
ui::Layer* layer = view_->layer();
|
||||
|
||||
layer->SetFillsBoundsOpaquely(false);
|
||||
|
||||
return layer;
|
||||
}
|
||||
|
||||
void View::SetBounds(const gfx::Rect& bounds, gin::Arguments* const args) {
|
||||
bool animate = false;
|
||||
int duration = 250;
|
||||
gfx::Tween::Type easing = gfx::Tween::LINEAR;
|
||||
|
||||
gin_helper::Dictionary dict;
|
||||
if (args->GetNext(&dict)) {
|
||||
v8::Local<v8::Value> animate_value;
|
||||
|
||||
if (dict.Get("animate", &animate_value)) {
|
||||
if (animate_value->IsBoolean()) {
|
||||
animate = animate_value->BooleanValue(isolate());
|
||||
} else {
|
||||
animate = true;
|
||||
|
||||
gin_helper::Dictionary animate_dict;
|
||||
if (gin::ConvertFromV8(isolate(), animate_value, &animate_dict)) {
|
||||
animate_dict.Get("duration", &duration);
|
||||
animate_dict.Get("easing", &easing);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (duration < 0)
|
||||
duration = 0;
|
||||
|
||||
if (!view_)
|
||||
return;
|
||||
view_->SetBoundsRect(bounds);
|
||||
|
||||
if (!animate) {
|
||||
view_->SetBoundsRect(bounds);
|
||||
return;
|
||||
}
|
||||
|
||||
ui::Layer* layer = GetLayer();
|
||||
|
||||
gfx::Rect current_bounds = view_->bounds();
|
||||
|
||||
if (bounds.size() == current_bounds.size()) {
|
||||
// If the size isn't changing, we can just animate the bounds directly.
|
||||
|
||||
views::AnimationBuilder()
|
||||
.SetPreemptionStrategy(
|
||||
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
|
||||
.OnEnded(base::BindOnce(
|
||||
[](views::View* view, const gfx::Rect& final_bounds) {
|
||||
view->SetBoundsRect(final_bounds);
|
||||
},
|
||||
view_, bounds))
|
||||
.Once()
|
||||
.SetDuration(base::Milliseconds(duration))
|
||||
.SetBounds(view_, bounds, easing);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
gfx::Rect target_size = gfx::Rect(0, 0, bounds.width(), bounds.height());
|
||||
gfx::Rect max_size =
|
||||
gfx::Rect(current_bounds.x(), current_bounds.y(),
|
||||
std::max(current_bounds.width(), bounds.width()),
|
||||
std::max(current_bounds.height(), bounds.height()));
|
||||
|
||||
// if the view's size is smaller than the target size, we need to set the
|
||||
// view's bounds immediatley to the new size (not position) and set the
|
||||
// layer's clip rect to animate from there.
|
||||
if (view_->width() < bounds.width() || view_->height() < bounds.height()) {
|
||||
view_->SetBoundsRect(max_size);
|
||||
|
||||
if (layer) {
|
||||
layer->SetClipRect(
|
||||
gfx::Rect(0, 0, current_bounds.width(), current_bounds.height()));
|
||||
}
|
||||
}
|
||||
|
||||
views::AnimationBuilder()
|
||||
.SetPreemptionStrategy(
|
||||
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
|
||||
.OnEnded(base::BindOnce(
|
||||
[](views::View* view, const gfx::Rect& final_bounds,
|
||||
ui::Layer* layer) {
|
||||
view->SetBoundsRect(final_bounds);
|
||||
if (layer)
|
||||
layer->SetClipRect(gfx::Rect());
|
||||
},
|
||||
view_, bounds, layer))
|
||||
.Once()
|
||||
.SetDuration(base::Milliseconds(duration))
|
||||
.SetBounds(view_, bounds, easing)
|
||||
.SetClipRect(
|
||||
view_, target_size,
|
||||
easing); // We have to set the clip rect independently of the
|
||||
// bounds, because animating the bounds of the layer
|
||||
// will not animate the underlying view's bounds.
|
||||
}
|
||||
|
||||
gfx::Rect View::GetBounds() const {
|
||||
|
||||
@@ -37,7 +37,7 @@ class View : public gin_helper::EventEmitter<View>,
|
||||
std::optional<size_t> index);
|
||||
void RemoveChildView(gin_helper::Handle<View> child);
|
||||
|
||||
void SetBounds(const gfx::Rect& bounds);
|
||||
void SetBounds(const gfx::Rect& bounds, gin::Arguments* args);
|
||||
gfx::Rect GetBounds() const;
|
||||
void SetLayout(v8::Isolate* isolate, v8::Local<v8::Object> value);
|
||||
std::vector<v8::Local<v8::Value>> GetChildren();
|
||||
@@ -70,6 +70,7 @@ class View : public gin_helper::EventEmitter<View>,
|
||||
void OnChildViewRemoved(views::View* observed_view,
|
||||
views::View* child) override;
|
||||
|
||||
ui::Layer* GetLayer();
|
||||
void ApplyBorderRadius();
|
||||
void ReorderChildView(gin_helper::Handle<View> child, size_t index);
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -484,14 +484,15 @@ NSArray* ConvertSharingItemToNS(const SharingItem& item) {
|
||||
if (index < 0 || index >= count)
|
||||
return;
|
||||
|
||||
// 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) || !isMenuOpen_;
|
||||
item.hidden = !model->IsVisibleAt(index);
|
||||
item.enabled = model->IsEnabledAt(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,6 +558,14 @@ NSArray* ConvertSharingItemToNS(const SharingItem& item) {
|
||||
|
||||
- (void)menuWillOpen:(NSMenu*)menu {
|
||||
isMenuOpen_ = YES;
|
||||
|
||||
// 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];
|
||||
}
|
||||
|
||||
[self refreshMenuTree:menu];
|
||||
if (model_)
|
||||
model_->MenuWillShow();
|
||||
@@ -567,18 +576,19 @@ NSArray* ConvertSharingItemToNS(const SharingItem& item) {
|
||||
if (!isMenuOpen_)
|
||||
return;
|
||||
|
||||
bool has_close_cb = !popupCloseCallback.is_null();
|
||||
isMenuOpen_ = NO;
|
||||
[self refreshMenuTree:menu];
|
||||
|
||||
// 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
|
||||
|
||||
@@ -49,6 +49,10 @@
|
||||
#include "ui/views/widget/widget.h"
|
||||
#include "ui/views/widget/widget_delegate.h"
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
#include "ui/accessibility/platform/ax_platform_node_win.h"
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::string_view kTargetsDataFile = "targets-data.json";
|
||||
@@ -151,6 +155,28 @@ bool ShouldHandleAccessibilityRequestCallback(const std::string& path) {
|
||||
return path == kTargetsDataFile;
|
||||
}
|
||||
|
||||
// Sets boolean values in `data` for each bit in `new_ax_mode` that differs from
|
||||
// that in `last_ax_mode`. Returns `true` if `data` was modified.
|
||||
void SetProcessModeBools(ui::AXMode ax_mode, base::Value::Dict& data) {
|
||||
data.Set(kNative, ax_mode.has_mode(ui::AXMode::kNativeAPIs));
|
||||
data.Set(kWeb, ax_mode.has_mode(ui::AXMode::kWebContents));
|
||||
data.Set(kText, ax_mode.has_mode(ui::AXMode::kInlineTextBoxes));
|
||||
data.Set(kExtendedProperties,
|
||||
ax_mode.has_mode(ui::AXMode::kExtendedProperties));
|
||||
data.Set(kHTML, ax_mode.has_mode(ui::AXMode::kHTML));
|
||||
data.Set(kScreenReader, ax_mode.has_mode(ui::AXMode::kScreenReader));
|
||||
}
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
// Sets values in `data` for the platform node counts in `counts`.
|
||||
void SetNodeCounts(const ui::AXPlatformNodeWin::Counts& counts,
|
||||
base::Value::Dict& data) {
|
||||
data.Set("dormantCount", base::NumberToString(counts.dormant_nodes));
|
||||
data.Set("liveCount", base::NumberToString(counts.live_nodes));
|
||||
data.Set("ghostCount", base::NumberToString(counts.ghost_nodes));
|
||||
}
|
||||
#endif
|
||||
|
||||
void HandleAccessibilityRequestCallback(
|
||||
content::BrowserContext* current_context,
|
||||
ui::AXMode initial_process_mode,
|
||||
@@ -294,6 +320,10 @@ void HandleAccessibilityRequestCallback(
|
||||
}
|
||||
data.Set(kBrowsersField, std::move(window_list));
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
SetNodeCounts(ui::AXPlatformNodeWin::GetCounts(), data);
|
||||
#endif
|
||||
|
||||
std::move(callback).Run(base::MakeRefCounted<base::RefCountedString>(
|
||||
base::WriteJson(data).value_or("")));
|
||||
}
|
||||
@@ -381,8 +411,13 @@ ElectronAccessibilityUI::ElectronAccessibilityUI(content::WebUI* web_ui)
|
||||
|
||||
ElectronAccessibilityUI::~ElectronAccessibilityUI() = default;
|
||||
|
||||
ElectronAccessibilityUIMessageHandler::ElectronAccessibilityUIMessageHandler() =
|
||||
default;
|
||||
ElectronAccessibilityUIMessageHandler::ElectronAccessibilityUIMessageHandler()
|
||||
: update_display_timer_(
|
||||
FROM_HERE,
|
||||
base::Seconds(1),
|
||||
base::BindRepeating(
|
||||
&ElectronAccessibilityUIMessageHandler::OnUpdateDisplayTimer,
|
||||
base::Unretained(this))) {}
|
||||
|
||||
void ElectronAccessibilityUIMessageHandler::GetRequestTypeAndFilters(
|
||||
const base::DictValue& data,
|
||||
@@ -472,6 +507,10 @@ void ElectronAccessibilityUIMessageHandler::RegisterMessages() {
|
||||
base::BindRepeating(
|
||||
&AccessibilityUIMessageHandler::RequestAccessibilityEvents,
|
||||
base::Unretained(this)));
|
||||
|
||||
auto* web_contents = web_ui()->GetWebContents();
|
||||
Observe(web_contents);
|
||||
OnVisibilityChanged(web_contents->GetVisibility());
|
||||
}
|
||||
|
||||
// static
|
||||
@@ -482,3 +521,45 @@ void ElectronAccessibilityUIMessageHandler::RegisterPrefs(
|
||||
registry->RegisterStringPref(prefs::kShownAccessibilityApiType,
|
||||
std::string(default_api_type));
|
||||
}
|
||||
|
||||
void ElectronAccessibilityUIMessageHandler::OnVisibilityChanged(
|
||||
content::Visibility visibility) {
|
||||
if (visibility == content::Visibility::HIDDEN) {
|
||||
update_display_timer_.Stop();
|
||||
} else {
|
||||
update_display_timer_.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
void ElectronAccessibilityUIMessageHandler::OnUpdateDisplayTimer() {
|
||||
// Collect the current state.
|
||||
base::Value::Dict data;
|
||||
|
||||
SetProcessModeBools(
|
||||
content::BrowserAccessibilityState::GetInstance()->GetAccessibilityMode(),
|
||||
data);
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
SetNodeCounts(ui::AXPlatformNodeWin::GetCounts(), data);
|
||||
#endif // BUILDFLAG(IS_WIN)
|
||||
|
||||
// Compute the delta from the last transmission.
|
||||
for (auto scan = data.begin(); scan != data.end();) {
|
||||
const auto& [new_key, new_value] = *scan;
|
||||
if (const auto* old_value = last_data_.Find(new_key);
|
||||
!old_value || *old_value != new_value) {
|
||||
// This is a new value; remember it for the future.
|
||||
last_data_.Set(new_key, new_value.Clone());
|
||||
++scan;
|
||||
} else {
|
||||
// This is the same as the last value; forget about it.
|
||||
scan = data.erase(scan);
|
||||
}
|
||||
}
|
||||
|
||||
// Transmit any new values to the UI.
|
||||
if (!data.empty()) {
|
||||
AllowJavascript();
|
||||
FireWebUIListener("updateDisplay", data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,21 @@ class ElectronAccessibilityUIMessageHandler
|
||||
std::string& allow,
|
||||
std::string& allow_empty,
|
||||
std::string& deny);
|
||||
|
||||
void RequestNativeUITree(const base::ListValue& args);
|
||||
|
||||
// content::WebContentsObserver:
|
||||
void OnVisibilityChanged(content::Visibility visibility) override;
|
||||
|
||||
// Updates the UI with new data. Called periodically to keep the UI up-to-date
|
||||
// while it is visible.
|
||||
void OnUpdateDisplayTimer();
|
||||
|
||||
// The last data for display sent to the UI by OnUpdateDisplayTimer.
|
||||
base::Value::Dict last_data_;
|
||||
|
||||
// A timer that runs while the UI is visible to call OnUpdateDisplayTimer.
|
||||
base::RepeatingTimer update_display_timer_;
|
||||
};
|
||||
|
||||
#endif // ELECTRON_SHELL_BROWSER_UI_WEBUI_ACCESSIBILITY_UI_H_
|
||||
|
||||
@@ -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_
|
||||
|
||||
@@ -68,6 +68,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) \
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
#include <fcntl.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <queue>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -27,8 +29,13 @@
|
||||
#include "base/run_loop.h"
|
||||
#include "base/strings/escape.h"
|
||||
#include "base/strings/string_util.h"
|
||||
#include "base/task/thread_pool.h"
|
||||
#include "base/threading/thread_restrictions.h"
|
||||
#include "base/types/expected.h"
|
||||
#include "components/dbus/thread_linux/dbus_thread_linux.h"
|
||||
#include "components/dbus/utils/call_method.h"
|
||||
#include "components/dbus/utils/check_for_service_and_start.h"
|
||||
#include "components/dbus/xdg/request.h"
|
||||
#include "content/public/browser/browser_thread.h"
|
||||
#include "dbus/bus.h"
|
||||
#include "dbus/message.h"
|
||||
@@ -45,9 +52,6 @@ void OpenFolder(const base::FilePath& full_path);
|
||||
|
||||
namespace {
|
||||
|
||||
const char kMethodListActivatableNames[] = "ListActivatableNames";
|
||||
const char kMethodNameHasOwner[] = "NameHasOwner";
|
||||
|
||||
const char kFreedesktopFileManagerName[] = "org.freedesktop.FileManager1";
|
||||
const char kFreedesktopFileManagerPath[] = "/org/freedesktop/FileManager1";
|
||||
|
||||
@@ -58,6 +62,7 @@ const char kFreedesktopPortalPath[] = "/org/freedesktop/portal/desktop";
|
||||
const char kFreedesktopPortalOpenURI[] = "org.freedesktop.portal.OpenURI";
|
||||
|
||||
const char kMethodOpenDirectory[] = "OpenDirectory";
|
||||
const char kActivationTokenKey[] = "activation_token";
|
||||
|
||||
class ShowItemHelper {
|
||||
public:
|
||||
@@ -72,179 +77,216 @@ class ShowItemHelper {
|
||||
ShowItemHelper& operator=(const ShowItemHelper&) = delete;
|
||||
|
||||
void ShowItemInFolder(const base::FilePath& full_path) {
|
||||
if (!bus_)
|
||||
if (!bus_) {
|
||||
bus_ = dbus_thread_linux::GetSharedSessionBus();
|
||||
|
||||
if (!dbus_proxy_) {
|
||||
dbus_proxy_ = bus_->GetObjectProxy(DBUS_SERVICE_DBUS,
|
||||
dbus::ObjectPath(DBUS_PATH_DBUS));
|
||||
}
|
||||
|
||||
if (prefer_filemanager_interface_.has_value()) {
|
||||
if (prefer_filemanager_interface_.value()) {
|
||||
ShowItemUsingFileManager(full_path);
|
||||
} else {
|
||||
ShowItemUsingFreedesktopPortal(full_path);
|
||||
}
|
||||
} else {
|
||||
CheckFileManagerRunning(full_path);
|
||||
if (api_type_.has_value()) {
|
||||
ShowItemInFolderOnApiTypeSet(full_path);
|
||||
return;
|
||||
}
|
||||
|
||||
bool api_availability_check_in_progress = !pending_requests_.empty();
|
||||
pending_requests_.push(full_path);
|
||||
if (!api_availability_check_in_progress) {
|
||||
// Initiate check to determine if portal or the FileManager API should
|
||||
// be used. The portal API is always preferred if available.
|
||||
dbus_utils::CheckForServiceAndStart(
|
||||
bus_.get(), kFreedesktopPortalName,
|
||||
base::BindOnce(&ShowItemHelper::CheckPortalRunningResponse,
|
||||
// Unretained is safe, the ShowItemHelper instance is
|
||||
// never destroyed.
|
||||
base::Unretained(this)));
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void CheckFileManagerRunning(const base::FilePath& full_path) {
|
||||
dbus::MethodCall method_call(DBUS_INTERFACE_DBUS, kMethodNameHasOwner);
|
||||
dbus::MessageWriter writer(&method_call);
|
||||
writer.AppendString(kFreedesktopFileManagerName);
|
||||
enum class ApiType { kNone, kPortal, kFileManager };
|
||||
|
||||
dbus_proxy_->CallMethod(
|
||||
&method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
|
||||
base::BindOnce(&ShowItemHelper::CheckFileManagerRunningResponse,
|
||||
base::Unretained(this), full_path));
|
||||
void ShowItemInFolderOnApiTypeSet(const base::FilePath& full_path) {
|
||||
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
|
||||
CHECK(api_type_.has_value());
|
||||
switch (*api_type_) {
|
||||
case ApiType::kPortal:
|
||||
ShowItemUsingPortal(full_path);
|
||||
break;
|
||||
case ApiType::kFileManager:
|
||||
ShowItemUsingFileManager(full_path);
|
||||
break;
|
||||
case ApiType::kNone:
|
||||
OpenParentFolderFallback(full_path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void CheckFileManagerRunningResponse(const base::FilePath& full_path,
|
||||
dbus::Response* response) {
|
||||
if (prefer_filemanager_interface_.has_value()) {
|
||||
ShowItemInFolder(full_path);
|
||||
void ProcessPendingRequests() {
|
||||
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
|
||||
if (!bus_) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool is_running = false;
|
||||
|
||||
if (!response) {
|
||||
LOG(ERROR) << "Failed to call " << kMethodNameHasOwner;
|
||||
} else {
|
||||
dbus::MessageReader reader(response);
|
||||
bool owned = false;
|
||||
|
||||
if (!reader.PopBool(&owned)) {
|
||||
LOG(ERROR) << "Failed to read " << kMethodNameHasOwner << " response";
|
||||
} else if (owned) {
|
||||
is_running = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_running) {
|
||||
prefer_filemanager_interface_ = true;
|
||||
ShowItemInFolder(full_path);
|
||||
} else {
|
||||
CheckFileManagerActivatable(full_path);
|
||||
CHECK(!pending_requests_.empty());
|
||||
while (!pending_requests_.empty()) {
|
||||
ShowItemInFolderOnApiTypeSet(pending_requests_.front());
|
||||
pending_requests_.pop();
|
||||
}
|
||||
}
|
||||
|
||||
void CheckFileManagerActivatable(const base::FilePath& full_path) {
|
||||
dbus::MethodCall method_call(DBUS_INTERFACE_DBUS,
|
||||
kMethodListActivatableNames);
|
||||
dbus_proxy_->CallMethod(
|
||||
&method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
|
||||
base::BindOnce(&ShowItemHelper::CheckFileManagerActivatableResponse,
|
||||
void CheckPortalRunningResponse(std::optional<bool> is_running) {
|
||||
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
|
||||
if (is_running.value_or(false)) {
|
||||
api_type_ = ApiType::kPortal;
|
||||
ProcessPendingRequests();
|
||||
} else {
|
||||
// Portal is unavailable.
|
||||
// Check if FileManager is available.
|
||||
dbus_utils::CheckForServiceAndStart(
|
||||
bus_.get(), kFreedesktopFileManagerName,
|
||||
base::BindOnce(&ShowItemHelper::CheckFileManagerRunningResponse,
|
||||
// Unretained is safe, the ShowItemHelper instance is
|
||||
// never destroyed.
|
||||
base::Unretained(this)));
|
||||
}
|
||||
}
|
||||
|
||||
void CheckFileManagerRunningResponse(std::optional<bool> is_running) {
|
||||
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
|
||||
if (is_running.value_or(false)) {
|
||||
api_type_ = ApiType::kFileManager;
|
||||
} else {
|
||||
// Neither portal nor FileManager is available.
|
||||
api_type_ = ApiType::kNone;
|
||||
}
|
||||
ProcessPendingRequests();
|
||||
}
|
||||
|
||||
void ShowItemUsingPortal(const base::FilePath& full_path) {
|
||||
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
|
||||
CHECK(api_type_.has_value());
|
||||
CHECK_EQ(*api_type_, ApiType::kPortal);
|
||||
base::ThreadPool::PostTaskAndReplyWithResult(
|
||||
FROM_HERE, {base::MayBlock()},
|
||||
base::BindOnce(
|
||||
[](const base::FilePath& full_path) {
|
||||
base::ScopedFD fd(HANDLE_EINTR(
|
||||
open(full_path.value().c_str(), O_RDONLY | O_CLOEXEC)));
|
||||
return fd;
|
||||
},
|
||||
full_path),
|
||||
base::BindOnce(&ShowItemHelper::ShowItemUsingPortalFdOpened,
|
||||
// Unretained is safe, the ShowItemHelper instance is
|
||||
// never destroyed.
|
||||
base::Unretained(this), full_path));
|
||||
}
|
||||
|
||||
void CheckFileManagerActivatableResponse(const base::FilePath& full_path,
|
||||
dbus::Response* response) {
|
||||
if (prefer_filemanager_interface_.has_value()) {
|
||||
ShowItemInFolder(full_path);
|
||||
void ShowItemUsingPortalFdOpened(const base::FilePath& full_path,
|
||||
base::ScopedFD fd) {
|
||||
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
|
||||
if (!bus_) {
|
||||
return;
|
||||
}
|
||||
if (!fd.is_valid()) {
|
||||
// At least open the parent folder, as long as we're not in the unit
|
||||
// tests.
|
||||
OpenParentFolderFallback(full_path);
|
||||
return;
|
||||
}
|
||||
base::nix::CreateXdgActivationToken(base::BindOnce(
|
||||
&ShowItemHelper::ShowItemUsingPortalWithToken,
|
||||
// Unretained is safe, the ShowItemHelper instance is never destroyed.
|
||||
base::Unretained(this), full_path, std::move(fd)));
|
||||
}
|
||||
|
||||
void ShowItemUsingPortalWithToken(const base::FilePath& full_path,
|
||||
base::ScopedFD fd,
|
||||
std::string activation_token) {
|
||||
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
|
||||
if (!bus_) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool is_activatable = false;
|
||||
|
||||
if (!response) {
|
||||
LOG(ERROR) << "Failed to call " << kMethodListActivatableNames;
|
||||
} else {
|
||||
dbus::MessageReader reader(response);
|
||||
std::vector<std::string> names;
|
||||
if (!reader.PopArrayOfStrings(&names)) {
|
||||
LOG(ERROR) << "Failed to read " << kMethodListActivatableNames
|
||||
<< " response";
|
||||
} else if (std::ranges::contains(names, kFreedesktopFileManagerName)) {
|
||||
is_activatable = true;
|
||||
}
|
||||
}
|
||||
|
||||
prefer_filemanager_interface_ = is_activatable;
|
||||
ShowItemInFolder(full_path);
|
||||
}
|
||||
|
||||
void ShowItemUsingFreedesktopPortal(const base::FilePath& full_path) {
|
||||
if (!object_proxy_) {
|
||||
object_proxy_ = bus_->GetObjectProxy(
|
||||
if (!portal_object_proxy_) {
|
||||
portal_object_proxy_ = bus_->GetObjectProxy(
|
||||
kFreedesktopPortalName, dbus::ObjectPath(kFreedesktopPortalPath));
|
||||
}
|
||||
|
||||
base::ScopedFD fd(
|
||||
HANDLE_EINTR(open(full_path.value().c_str(), O_RDONLY | O_CLOEXEC)));
|
||||
if (!fd.is_valid()) {
|
||||
LOG(ERROR) << "Failed to open " << full_path << " for URI portal";
|
||||
dbus_xdg::Dictionary options;
|
||||
options[kActivationTokenKey] =
|
||||
dbus_utils::Variant::Wrap<"s">(activation_token);
|
||||
// In the rare occasion that another request comes in before the response is
|
||||
// received, we will end up overwriting this request object with the new one
|
||||
// and the response from the first request will not be handled in that case.
|
||||
// This should be acceptable as it means the two requests were received too
|
||||
// close to each other from the user and the first one was handled on a best
|
||||
// effort basis.
|
||||
portal_open_directory_request_ = std::make_unique<dbus_xdg::Request>(
|
||||
bus_, portal_object_proxy_, kFreedesktopPortalOpenURI,
|
||||
kMethodOpenDirectory, std::move(options),
|
||||
base::BindOnce(&ShowItemHelper::ShowItemUsingPortalResponse,
|
||||
// Unretained is safe, the ShowItemHelper instance is
|
||||
// never destroyed.
|
||||
base::Unretained(this), full_path),
|
||||
std::string(), std::move(fd));
|
||||
}
|
||||
|
||||
// If the call fails, at least open the parent folder.
|
||||
platform_util::OpenFolder(full_path.DirName());
|
||||
|
||||
return;
|
||||
void ShowItemUsingPortalResponse(
|
||||
const base::FilePath& full_path,
|
||||
base::expected<dbus_xdg::Dictionary, dbus_xdg::ResponseError> results) {
|
||||
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
|
||||
portal_open_directory_request_.reset();
|
||||
if (!results.has_value()) {
|
||||
OpenParentFolderFallback(full_path);
|
||||
}
|
||||
|
||||
dbus::MethodCall open_directory_call(kFreedesktopPortalOpenURI,
|
||||
kMethodOpenDirectory);
|
||||
dbus::MessageWriter writer(&open_directory_call);
|
||||
|
||||
writer.AppendString("");
|
||||
|
||||
// Note that AppendFileDescriptor() duplicates the fd, so we shouldn't
|
||||
// release ownership of it here.
|
||||
writer.AppendFileDescriptor(fd.get());
|
||||
|
||||
dbus::MessageWriter options_writer(nullptr);
|
||||
writer.OpenArray("{sv}", &options_writer);
|
||||
writer.CloseContainer(&options_writer);
|
||||
|
||||
ShowItemUsingBusCall(&open_directory_call, full_path);
|
||||
}
|
||||
|
||||
void ShowItemUsingFileManager(const base::FilePath& full_path) {
|
||||
if (!object_proxy_) {
|
||||
object_proxy_ =
|
||||
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
|
||||
if (!bus_) {
|
||||
return;
|
||||
}
|
||||
CHECK(api_type_.has_value());
|
||||
CHECK_EQ(*api_type_, ApiType::kFileManager);
|
||||
if (!file_manager_object_proxy_) {
|
||||
file_manager_object_proxy_ =
|
||||
bus_->GetObjectProxy(kFreedesktopFileManagerName,
|
||||
dbus::ObjectPath(kFreedesktopFileManagerPath));
|
||||
}
|
||||
|
||||
dbus::MethodCall show_items_call(kFreedesktopFileManagerName,
|
||||
kMethodShowItems);
|
||||
dbus::MessageWriter writer(&show_items_call);
|
||||
|
||||
writer.AppendArrayOfStrings(
|
||||
{"file://" + base::EscapePath(
|
||||
full_path.value())}); // List of file(s) to highlight.
|
||||
writer.AppendString({}); // startup-id
|
||||
|
||||
ShowItemUsingBusCall(&show_items_call, full_path);
|
||||
std::vector<std::string> file_to_highlight{"file://" + full_path.value()};
|
||||
dbus_utils::CallMethod<"ass", "">(
|
||||
file_manager_object_proxy_, kFreedesktopFileManagerName,
|
||||
kMethodShowItems,
|
||||
base::BindOnce(&ShowItemHelper::ShowItemUsingFileManagerResponse,
|
||||
// Unretained is safe, the ShowItemHelper instance is
|
||||
// never destroyed.
|
||||
base::Unretained(this), full_path),
|
||||
std::move(file_to_highlight), /*startup-id=*/"");
|
||||
}
|
||||
|
||||
void ShowItemUsingBusCall(dbus::MethodCall* call,
|
||||
const base::FilePath& full_path) {
|
||||
object_proxy_->CallMethod(
|
||||
call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
|
||||
base::BindOnce(&ShowItemHelper::ShowItemInFolderResponse,
|
||||
base::Unretained(this), full_path, call->GetMember()));
|
||||
void ShowItemUsingFileManagerResponse(
|
||||
const base::FilePath& full_path,
|
||||
dbus_utils::CallMethodResultSig<""> response) {
|
||||
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
|
||||
if (!response.has_value()) {
|
||||
// If the bus call fails, at least open the parent folder.
|
||||
OpenParentFolderFallback(full_path);
|
||||
}
|
||||
}
|
||||
|
||||
void ShowItemInFolderResponse(const base::FilePath& full_path,
|
||||
const std::string& method,
|
||||
dbus::Response* response) {
|
||||
if (response)
|
||||
return;
|
||||
|
||||
LOG(ERROR) << "Error calling " << method;
|
||||
// If the bus call fails, at least open the parent folder.
|
||||
void OpenParentFolderFallback(const base::FilePath& full_path) {
|
||||
platform_util::OpenFolder(full_path.DirName());
|
||||
}
|
||||
|
||||
scoped_refptr<dbus::Bus> bus_;
|
||||
raw_ptr<dbus::ObjectProxy> dbus_proxy_ = nullptr;
|
||||
raw_ptr<dbus::ObjectProxy> object_proxy_ = nullptr;
|
||||
|
||||
std::optional<bool> prefer_filemanager_interface_;
|
||||
std::optional<ApiType> api_type_;
|
||||
// The proxy objects are owned by `bus_`.
|
||||
raw_ptr<dbus::ObjectProxy> portal_object_proxy_ = nullptr;
|
||||
raw_ptr<dbus::ObjectProxy> file_manager_object_proxy_ = nullptr;
|
||||
std::unique_ptr<dbus_xdg::Request> portal_open_directory_request_;
|
||||
|
||||
// Requests that are queued until the API availability is determined.
|
||||
std::queue<base::FilePath> pending_requests_;
|
||||
};
|
||||
|
||||
// Descriptions pulled from https://linux.die.net/man/1/xdg-open
|
||||
|
||||
@@ -2,7 +2,6 @@ import { app, BrowserWindow, Menu, session, net as electronNet, WebContents, uti
|
||||
|
||||
import { assert, expect } from 'chai';
|
||||
import * as semver from 'semver';
|
||||
import split = require('split')
|
||||
|
||||
import * as cp from 'node:child_process';
|
||||
import { once } from 'node:events';
|
||||
@@ -11,6 +10,7 @@ import * as http from 'node:http';
|
||||
import * as https from 'node:https';
|
||||
import * as net from 'node:net';
|
||||
import * as path from 'node:path';
|
||||
import * as readline from 'node:readline';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
@@ -260,11 +260,11 @@ describe('app module', () => {
|
||||
const firstExited = once(first, 'exit');
|
||||
|
||||
// Wait for the first app to boot.
|
||||
const firstStdoutLines = first.stdout.pipe(split());
|
||||
while ((await once(firstStdoutLines, 'data')).toString() !== 'started') {
|
||||
const firstStdoutLines = readline.createInterface({ input: first.stdout });
|
||||
while ((await once(firstStdoutLines, 'line')).toString() !== 'started') {
|
||||
// wait.
|
||||
}
|
||||
const additionalDataPromise = once(firstStdoutLines, 'data');
|
||||
const additionalDataPromise = once(firstStdoutLines, 'line');
|
||||
|
||||
const secondInstanceArgs = [process.execPath, appPath, ...testArgs.args, '--some-switch', 'some-arg'];
|
||||
const second = cp.spawn(secondInstanceArgs[0], secondInstanceArgs.slice(1));
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -90,7 +90,7 @@ describe('BrowserView module', () => {
|
||||
w.show();
|
||||
w.setBounds(display.bounds);
|
||||
w.setBackgroundColor(WINDOW_BACKGROUND_COLOR);
|
||||
await w.loadURL('about:blank');
|
||||
await w.loadURL('data:text/html,<html></html>');
|
||||
|
||||
view = new BrowserView();
|
||||
view.setBounds(display.bounds);
|
||||
@@ -109,7 +109,7 @@ describe('BrowserView module', () => {
|
||||
w.show();
|
||||
w.setBounds(display.bounds);
|
||||
w.setBackgroundColor(WINDOW_BACKGROUND_COLOR);
|
||||
await w.loadURL('about:blank');
|
||||
await w.loadURL('data:text/html,<html></html>');
|
||||
|
||||
view = new BrowserView();
|
||||
view.setBounds(display.bounds);
|
||||
|
||||
@@ -6933,7 +6933,7 @@ describe('BrowserWindow module', () => {
|
||||
hasShadow: false
|
||||
});
|
||||
|
||||
await backgroundWindow.loadURL('about:blank');
|
||||
await backgroundWindow.loadURL('data:text/html,<html></html>');
|
||||
|
||||
const foregroundWindow = new BrowserWindow({
|
||||
...display.bounds,
|
||||
@@ -6974,7 +6974,7 @@ describe('BrowserWindow module', () => {
|
||||
hasShadow: false
|
||||
});
|
||||
|
||||
await backgroundWindow.loadURL('about:blank');
|
||||
await backgroundWindow.loadURL('data:text/html,<html></html>');
|
||||
|
||||
const foregroundWindow = new BrowserWindow({
|
||||
...display.bounds,
|
||||
@@ -7027,7 +7027,7 @@ describe('BrowserWindow module', () => {
|
||||
backgroundColor: HexColors.BLUE
|
||||
});
|
||||
|
||||
w.loadURL('about:blank');
|
||||
w.loadURL('data:text/html,<html></html>');
|
||||
await once(w, 'ready-to-show');
|
||||
|
||||
const screenCapture = new ScreenCapture(display);
|
||||
|
||||
@@ -214,7 +214,7 @@ describe('session module', () => {
|
||||
|
||||
expect(setEventCookie.name).to.equal(name);
|
||||
expect(setEventCookie.value).to.equal(value);
|
||||
expect(setEventCause).to.equal('explicit');
|
||||
expect(setEventCause).to.equal('inserted');
|
||||
expect(setEventRemoved).to.equal(false);
|
||||
|
||||
expect(removeEventCookie.name).to.equal(name);
|
||||
|
||||
@@ -133,5 +133,18 @@ describe('View', () => {
|
||||
parent.setBounds({ x: 50, y: 60, width: 500, height: 600 });
|
||||
expect(child.getBounds()).to.deep.equal({ x: 10, y: 15, width: 25, height: 30 });
|
||||
});
|
||||
|
||||
it('can set bounds with animation', (done) => {
|
||||
const v = new View();
|
||||
v.setBounds({ x: 0, y: 0, width: 100, height: 100 }, {
|
||||
animate: {
|
||||
duration: 300
|
||||
}
|
||||
});
|
||||
setTimeout(() => {
|
||||
expect(v.getBounds()).to.deep.equal({ x: 0, y: 0, width: 100, height: 100 });
|
||||
done();
|
||||
}, 350);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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.
BIN
spec/fixtures/api/autoupdater/msix/HelloMSIX_V2.msix
vendored
Normal file
BIN
spec/fixtures/api/autoupdater/msix/HelloMSIX_V2.msix
vendored
Normal file
Binary file not shown.
BIN
spec/fixtures/api/autoupdater/msix/MSIXDevCert.cer
vendored
Normal file
BIN
spec/fixtures/api/autoupdater/msix/MSIXDevCert.cer
vendored
Normal file
Binary file not shown.
22
spec/fixtures/api/autoupdater/msix/install_test_cert.ps1
vendored
Normal file
22
spec/fixtures/api/autoupdater/msix/install_test_cert.ps1
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
Add-Type -AssemblyName System.Security.Cryptography.X509Certificates
|
||||
|
||||
# Path to cert file one folder up relative to script location
|
||||
$scriptDir = Split-Path -Parent $PSCommandPath
|
||||
$certPath = Join-Path $scriptDir "MSIXDevCert.cer" | Resolve-Path
|
||||
|
||||
# Load the certificate from file
|
||||
$cert = [System.Security.Cryptography.X509Certificates.X509CertificateLoader]::LoadCertificateFromFile($certPath)
|
||||
|
||||
$trustedStore = Get-ChildItem -Path "cert:\LocalMachine\TrustedPeople" | Where-Object { $_.Thumbprint -eq $cert.Thumbprint }
|
||||
if (-not $trustedStore) {
|
||||
# We gonna need admin privileges to install the cert
|
||||
if (-Not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
||||
Start-Process powershell -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`"" -Verb RunAs
|
||||
exit
|
||||
}
|
||||
# Install the public cert to LocalMachine\TrustedPeople (for MSIX trust)
|
||||
Import-Certificate -FilePath $certPath -CertStoreLocation "cert:\LocalMachine\TrustedPeople" | Out-Null
|
||||
Write-Host " 🏛️ Installed to: cert:\LocalMachine\TrustedPeople"
|
||||
} else {
|
||||
Write-Host " ✅ Certificate already trusted in: cert:\LocalMachine\TrustedPeople"
|
||||
}
|
||||
96
spec/fixtures/api/autoupdater/msix/main.js
vendored
Normal file
96
spec/fixtures/api/autoupdater/msix/main.js
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
const { app, autoUpdater } = require('electron');
|
||||
|
||||
// Parse command-line arguments
|
||||
const args = process.argv.slice(2);
|
||||
const command = args[0];
|
||||
const commandArg = args[1];
|
||||
|
||||
app.whenReady().then(() => {
|
||||
try {
|
||||
// Debug: log received arguments
|
||||
if (process.env.DEBUG) {
|
||||
console.log('Command:', command);
|
||||
console.log('Command arg:', commandArg);
|
||||
console.log('All args:', JSON.stringify(args));
|
||||
}
|
||||
|
||||
if (command === '--printPackageId') {
|
||||
const packageInfo = autoUpdater.getPackageInfo();
|
||||
if (packageInfo.familyName) {
|
||||
console.log(`Family Name: ${packageInfo.familyName}`);
|
||||
console.log(`Package ID: ${packageInfo.id || 'N/A'}`);
|
||||
console.log(`Version: ${packageInfo.version || 'N/A'}`);
|
||||
console.log(`Development Mode: ${packageInfo.developmentMode ? 'Yes' : 'No'}`);
|
||||
console.log(`Signature Kind: ${packageInfo.signatureKind || 'N/A'}`);
|
||||
if (packageInfo.appInstallerUri) {
|
||||
console.log(`App Installer URI: ${packageInfo.appInstallerUri}`);
|
||||
}
|
||||
app.quit();
|
||||
} else {
|
||||
console.error('No package identity found. Process is not running in an MSIX package context.');
|
||||
app.exit(1);
|
||||
}
|
||||
} else if (command === '--checkUpdate') {
|
||||
if (!commandArg) {
|
||||
console.error('Update URL is required for --checkUpdate');
|
||||
app.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Use hardcoded headers if --useCustomHeaders flag is provided
|
||||
let headers;
|
||||
let allowAnyVersion = false;
|
||||
if (args[2] === '--useCustomHeaders') {
|
||||
headers = {
|
||||
'X-AppVersion': '1.0.0',
|
||||
Authorization: 'Bearer test-token'
|
||||
};
|
||||
} else if (args[2] === '--allowAnyVersion') {
|
||||
allowAnyVersion = true;
|
||||
}
|
||||
|
||||
// Set up event listeners
|
||||
autoUpdater.on('checking-for-update', () => {
|
||||
console.log('Checking for update...');
|
||||
});
|
||||
|
||||
autoUpdater.on('update-available', () => {
|
||||
console.log('Update available');
|
||||
});
|
||||
|
||||
autoUpdater.on('update-not-available', () => {
|
||||
console.log('Update not available');
|
||||
app.quit();
|
||||
});
|
||||
|
||||
autoUpdater.on('update-downloaded', (event, releaseNotes, releaseName, releaseDate, updateUrl) => {
|
||||
console.log('Update downloaded');
|
||||
console.log(`Release Name: ${releaseName || 'N/A'}`);
|
||||
console.log(`Release Notes: ${releaseNotes || 'N/A'}`);
|
||||
console.log(`Release Date: ${releaseDate || 'N/A'}`);
|
||||
console.log(`Update URL: ${updateUrl || 'N/A'}`);
|
||||
app.quit();
|
||||
});
|
||||
|
||||
autoUpdater.on('error', (error, message) => {
|
||||
console.error(`Update error: ${message || error.message || 'Unknown error'}`);
|
||||
app.exit(1);
|
||||
});
|
||||
|
||||
// Set the feed URL with optional headers and allowAnyVersion, then check for updates
|
||||
if (headers || allowAnyVersion) {
|
||||
autoUpdater.setFeedURL({ url: commandArg, headers, allowAnyVersion });
|
||||
} else {
|
||||
autoUpdater.setFeedURL(commandArg);
|
||||
}
|
||||
autoUpdater.checkForUpdates();
|
||||
} else {
|
||||
console.error(`Unknown command: ${command || '(none)'}`);
|
||||
app.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Unhandled error:', error.message);
|
||||
console.error(error.stack);
|
||||
app.exit(1);
|
||||
}
|
||||
});
|
||||
149
spec/lib/msix-helpers.ts
Normal file
149
spec/lib/msix-helpers.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { expect } from 'chai';
|
||||
|
||||
import * as cp from 'node:child_process';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
||||
const fixturesPath = path.resolve(__dirname, '..', 'fixtures', 'api', 'autoupdater', 'msix');
|
||||
const manifestFixturePath = path.resolve(fixturesPath, 'ElectronDevAppxManifest.xml');
|
||||
const installCertScriptPath = path.resolve(fixturesPath, 'install_test_cert.ps1');
|
||||
|
||||
// Install the signing certificate for MSIX test packages to the Trusted People store
|
||||
// This is required to install self-signed MSIX packages
|
||||
export async function installMsixCertificate (): Promise<void> {
|
||||
const result = cp.spawnSync('powershell', [
|
||||
'-NoProfile',
|
||||
'-ExecutionPolicy', 'Bypass',
|
||||
'-File', installCertScriptPath
|
||||
]);
|
||||
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`Failed to install MSIX certificate: ${result.stderr.toString() || result.stdout.toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we should run MSIX tests
|
||||
export const shouldRunMsixTests =
|
||||
process.platform === 'win32';
|
||||
|
||||
// Get the Electron executable path
|
||||
export function getElectronExecutable (): string {
|
||||
return process.execPath;
|
||||
}
|
||||
|
||||
// Get path to main.js fixture
|
||||
export function getMainJsFixturePath (): string {
|
||||
return path.resolve(fixturesPath, 'main.js');
|
||||
}
|
||||
|
||||
// Register executable with identity
|
||||
export async function registerExecutableWithIdentity (executablePath: string): Promise<void> {
|
||||
if (!fs.existsSync(manifestFixturePath)) {
|
||||
throw new Error(`Manifest fixture not found: ${manifestFixturePath}`);
|
||||
}
|
||||
|
||||
const executableDir = path.dirname(executablePath);
|
||||
const manifestPath = path.join(executableDir, 'AppxManifest.xml');
|
||||
const escapedManifestPath = manifestPath.replace(/'/g, "''").replace(/\\/g, '\\\\');
|
||||
const psCommand = `
|
||||
$ErrorActionPreference = 'Stop';
|
||||
try {
|
||||
Add-AppxPackage -Register '${escapedManifestPath}' -ForceUpdateFromAnyVersion
|
||||
} catch {
|
||||
Write-Error $_.Exception.Message
|
||||
exit 1
|
||||
}
|
||||
`;
|
||||
|
||||
fs.copyFileSync(manifestFixturePath, manifestPath);
|
||||
|
||||
const result = cp.spawnSync('powershell', ['-NoProfile', '-Command', psCommand]);
|
||||
if (result.status !== 0) {
|
||||
const errorMsg = result.stderr.toString() || result.stdout.toString();
|
||||
try {
|
||||
fs.unlinkSync(manifestPath);
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
throw new Error(`Failed to register executable with identity: ${errorMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Unregister the Electron Dev MSIX package
|
||||
// This removes the sparse package registration created by registerExecutableWithIdentity
|
||||
export async function unregisterExecutableWithIdentity (): Promise<void> {
|
||||
const result = cp.spawnSync('powershell', ['-NoProfile', '-Command', ' Get-AppxPackage Electron.Dev.MSIX | Remove-AppxPackage']);
|
||||
// Don't throw if package doesn't exist
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`Failed to unregister executable with identity: ${result.stderr.toString() || result.stdout.toString()}`);
|
||||
}
|
||||
|
||||
const electronExec = getElectronExecutable();
|
||||
const executableDir = path.dirname(electronExec);
|
||||
const manifestPath = path.join(executableDir, 'AppxManifest.xml');
|
||||
try {
|
||||
if (fs.existsSync(manifestPath)) {
|
||||
fs.unlinkSync(manifestPath);
|
||||
}
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
|
||||
// Get path to MSIX fixture package
|
||||
export function getMsixFixturePath (version: 'v1' | 'v2'): string {
|
||||
const filename = `HelloMSIX_${version}.msix`;
|
||||
return path.resolve(fixturesPath, filename);
|
||||
}
|
||||
|
||||
// Install MSIX package
|
||||
export async function installMsixPackage (msixPath: string): Promise<void> {
|
||||
// Use Add-AppxPackage PowerShell cmdlet
|
||||
const result = cp.spawnSync('powershell', [
|
||||
'-Command',
|
||||
`Add-AppxPackage -Path "${msixPath}" -ForceApplicationShutdown`
|
||||
]);
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`Failed to install MSIX package: ${result.stderr.toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Uninstall MSIX package by name
|
||||
export async function uninstallMsixPackage (name: string): Promise<void> {
|
||||
const result = cp.spawnSync('powershell', ['-NoProfile', '-Command', `Get-AppxPackage ${name} | Remove-AppxPackage`]);
|
||||
// Don't throw if package doesn't exist
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`Failed to uninstall MSIX package: ${result.stderr.toString() || result.stdout.toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Get version of installed MSIX package by name
|
||||
export async function getMsixPackageVersion (name: string): Promise<string | null> {
|
||||
const psCommand = `(Get-AppxPackage -Name '${name}').Version`;
|
||||
const result = cp.spawnSync('powershell', ['-NoProfile', '-Command', psCommand]);
|
||||
if (result.status !== 0) {
|
||||
return null;
|
||||
}
|
||||
const version = result.stdout.toString().trim();
|
||||
return version || null;
|
||||
}
|
||||
|
||||
export function spawn (cmd: string, args: string[], opts: any = {}): Promise<{ code: number, out: string }> {
|
||||
let out = '';
|
||||
const child = cp.spawn(cmd, args, opts);
|
||||
child.stdout.on('data', (chunk: Buffer) => {
|
||||
out += chunk.toString();
|
||||
});
|
||||
child.stderr.on('data', (chunk: Buffer) => {
|
||||
out += chunk.toString();
|
||||
});
|
||||
return new Promise<{ code: number, out: string }>((resolve) => {
|
||||
child.on('exit', (code, signal) => {
|
||||
expect(signal).to.equal(null);
|
||||
resolve({
|
||||
code: code!,
|
||||
out
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -23,7 +23,6 @@
|
||||
"@types/mocha": "^7.0.2",
|
||||
"@types/send": "^0.14.5",
|
||||
"@types/sinon": "^9.0.4",
|
||||
"@types/split": "^1.0.5",
|
||||
"@types/uuid": "^3.4.6",
|
||||
"@types/w3c-web-serial": "^1.0.7",
|
||||
"@types/ws": "^7.2.0",
|
||||
@@ -46,7 +45,6 @@
|
||||
"q": "^1.5.1",
|
||||
"send": "^0.19.0",
|
||||
"sinon": "^9.0.1",
|
||||
"split": "^1.0.1",
|
||||
"uuid": "^3.3.3",
|
||||
"winreg": "1.2.4",
|
||||
"ws": "^7.5.10",
|
||||
|
||||
47
yarn.lock
47
yarn.lock
@@ -592,7 +592,7 @@ __metadata:
|
||||
"@electron/docs-parser": "npm:^2.0.0"
|
||||
"@electron/fiddle-core": "npm:^1.3.4"
|
||||
"@electron/github-app-auth": "npm:^3.2.0"
|
||||
"@electron/lint-roller": "npm:^3.1.2"
|
||||
"@electron/lint-roller": "npm:^3.2.0"
|
||||
"@electron/typescript-definitions": "npm:^9.1.5"
|
||||
"@octokit/rest": "npm:^20.1.2"
|
||||
"@primer/octicons": "npm:^10.0.0"
|
||||
@@ -869,9 +869,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@electron/lint-roller@npm:^3.1.2":
|
||||
version: 3.1.2
|
||||
resolution: "@electron/lint-roller@npm:3.1.2"
|
||||
"@electron/lint-roller@npm:^3.2.0":
|
||||
version: 3.2.0
|
||||
resolution: "@electron/lint-roller@npm:3.2.0"
|
||||
dependencies:
|
||||
"@dsanders11/vscode-markdown-languageservice": "npm:^0.3.0"
|
||||
ajv: "npm:^8.16.0"
|
||||
@@ -896,7 +896,7 @@ __metadata:
|
||||
lint-roller-markdown-links: dist/bin/lint-markdown-links.js
|
||||
lint-roller-markdown-standard: dist/bin/lint-markdown-standard.js
|
||||
lint-roller-markdown-ts-check: dist/bin/lint-markdown-ts-check.js
|
||||
checksum: 10c0/e98a65d44b99accf44e33d7dfb1e8d85a75580a7efa7b4a570b0f8e7274aad3eb60fc6d285b5d1d78ecd093e902820222cfdf3e193eec65a8a3536d4c75bb060
|
||||
checksum: 10c0/5331163518f41c1ea8ac302891d808891b5570f657587194717e3a8214c0d99e04949cf500ff138b02b77a373e317c3e77042b19c7fbda186d5c970b6b81df80
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -2286,16 +2286,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/split@npm:^1.0.5":
|
||||
version: 1.0.5
|
||||
resolution: "@types/split@npm:1.0.5"
|
||||
dependencies:
|
||||
"@types/node": "npm:*"
|
||||
"@types/through": "npm:*"
|
||||
checksum: 10c0/eb187a3b07e5064928e49bffd5c45ad1f1109135fee52344bb7623cdb55e2ebb16bd6ca009a30a0a6e2b262f7ebb7bf18030ff873819e80fafd4cbb51dba1a74
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/stream-chain@npm:*":
|
||||
version: 2.0.0
|
||||
resolution: "@types/stream-chain@npm:2.0.0"
|
||||
@@ -2338,15 +2328,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/through@npm:*":
|
||||
version: 0.0.33
|
||||
resolution: "@types/through@npm:0.0.33"
|
||||
dependencies:
|
||||
"@types/node": "npm:*"
|
||||
checksum: 10c0/6a8edd7f40cd7e197318e86310a40e568cddd380609dde59b30d5cc6c5f8276ddc698905eac4b3b429eb39f2e8ee326bc20dc6e95a2cdc41c4d3fc9a1ebd4929
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/unist@npm:*, @types/unist@npm:^2.0.0":
|
||||
version: 2.0.6
|
||||
resolution: "@types/unist@npm:2.0.6"
|
||||
@@ -4933,7 +4914,6 @@ __metadata:
|
||||
"@types/mocha": "npm:^7.0.2"
|
||||
"@types/send": "npm:^0.14.5"
|
||||
"@types/sinon": "npm:^9.0.4"
|
||||
"@types/split": "npm:^1.0.5"
|
||||
"@types/uuid": "npm:^3.4.6"
|
||||
"@types/w3c-web-serial": "npm:^1.0.7"
|
||||
"@types/ws": "npm:^7.2.0"
|
||||
@@ -4956,7 +4936,6 @@ __metadata:
|
||||
q: "npm:^1.5.1"
|
||||
send: "npm:^0.19.0"
|
||||
sinon: "npm:^9.0.1"
|
||||
split: "npm:^1.0.1"
|
||||
uuid: "npm:^3.3.3"
|
||||
winreg: "npm:1.2.4"
|
||||
ws: "npm:^7.5.10"
|
||||
@@ -6896,16 +6875,16 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"glob@npm:^7.1.3, glob@npm:^7.1.6":
|
||||
version: 7.2.0
|
||||
resolution: "glob@npm:7.2.0"
|
||||
version: 7.2.3
|
||||
resolution: "glob@npm:7.2.3"
|
||||
dependencies:
|
||||
fs.realpath: "npm:^1.0.0"
|
||||
inflight: "npm:^1.0.4"
|
||||
inherits: "npm:2"
|
||||
minimatch: "npm:^3.0.4"
|
||||
minimatch: "npm:^3.1.1"
|
||||
once: "npm:^1.3.0"
|
||||
path-is-absolute: "npm:^1.0.0"
|
||||
checksum: 10c0/478b40e38be5a3d514e64950e1e07e0ac120585add6a37c98d0ed24d72d9127d734d2a125786073c8deb687096e84ae82b641c441a869ada3a9cc91b68978632
|
||||
checksum: 10c0/65676153e2b0c9095100fe7f25a778bf45608eeb32c6048cf307f579649bcc30353277b3b898a3792602c65764e5baa4f643714dfbdfd64ea271d210c7a425fe
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -8706,9 +8685,9 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"lodash@npm:^4.0.0, lodash@npm:^4.17.11, lodash@npm:^4.17.15, lodash@npm:^4.17.21":
|
||||
version: 4.17.21
|
||||
resolution: "lodash@npm:4.17.21"
|
||||
checksum: 10c0/d8cbea072bb08655bb4c989da418994b073a608dffa608b09ac04b43a791b12aeae7cd7ad919aa4c925f33b48490b5cfe6c1f71d827956071dae2e7bb3a6b74c
|
||||
version: 4.17.23
|
||||
resolution: "lodash@npm:4.17.23"
|
||||
checksum: 10c0/1264a90469f5bb95d4739c43eb6277d15b6d9e186df4ac68c3620443160fc669e2f14c11e7d8b2ccf078b81d06147c01a8ccced9aab9f9f63d50dcf8cace6bf6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -9681,7 +9660,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"minimatch@npm:^3.0.2, minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.2":
|
||||
"minimatch@npm:^3.0.2, minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2":
|
||||
version: 3.1.2
|
||||
resolution: "minimatch@npm:3.1.2"
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user