From ae43f17b79b5498a0754bf247c0ea5dea57effe5 Mon Sep 17 00:00:00 2001 From: Felix Rieseberg Date: Mon, 26 Jan 2026 11:35:11 -0800 Subject: [PATCH 01/38] build: Run gh workflows only on e/e (#49521) --- .github/workflows/audit-branch-ci.yml | 1 + .github/workflows/branch-created.yml | 2 +- .github/workflows/build-git-cache.yml | 3 +++ .github/workflows/build.yml | 3 ++- .github/workflows/clean-src-cache.yml | 1 + .github/workflows/linux-publish.yml | 1 + .github/workflows/macos-disk-cleanup.yml | 1 + .github/workflows/macos-publish.yml | 1 + .github/workflows/scorecards.yml | 1 + .github/workflows/stable-prep-items.yml | 1 + .github/workflows/stale.yml | 3 ++- .github/workflows/windows-publish.yml | 1 + 12 files changed, 16 insertions(+), 3 deletions(-) diff --git a/.github/workflows/audit-branch-ci.yml b/.github/workflows/audit-branch-ci.yml index 14c31d01c6..8e879e5113 100644 --- a/.github/workflows/audit-branch-ci.yml +++ b/.github/workflows/audit-branch-ci.yml @@ -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 diff --git a/.github/workflows/branch-created.yml b/.github/workflows/branch-created.yml index 43e091b950..a43463c9b8 100644 --- a/.github/workflows/branch-created.yml +++ b/.github/workflows/branch-created.yml @@ -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 diff --git a/.github/workflows/build-git-cache.yml b/.github/workflows/build-git-cache.yml index b050b2e65a..0f92280c6d 100644 --- a/.github/workflows/build-git-cache.yml +++ b/.github/workflows/build-git-cache.yml @@ -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 @@ -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 @@ -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 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f4081658cc..2ee7847b42 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,6 +47,7 @@ permissions: {} jobs: setup: + if: github.repository == 'electron/electron' runs-on: ubuntu-latest permissions: contents: read @@ -426,7 +427,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: | diff --git a/.github/workflows/clean-src-cache.yml b/.github/workflows/clean-src-cache.yml index d632ba2d5f..c984b766b3 100644 --- a/.github/workflows/clean-src-cache.yml +++ b/.github/workflows/clean-src-cache.yml @@ -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 diff --git a/.github/workflows/linux-publish.yml b/.github/workflows/linux-publish.yml index ee15cca5e3..a8434e1375 100644 --- a/.github/workflows/linux-publish.yml +++ b/.github/workflows/linux-publish.yml @@ -21,6 +21,7 @@ permissions: {} jobs: checkout-linux: + if: github.repository == 'electron/electron' runs-on: electron-arc-centralus-linux-amd64-32core permissions: contents: read diff --git a/.github/workflows/macos-disk-cleanup.yml b/.github/workflows/macos-disk-cleanup.yml index a3a85beab8..217c446a48 100644 --- a/.github/workflows/macos-disk-cleanup.yml +++ b/.github/workflows/macos-disk-cleanup.yml @@ -13,6 +13,7 @@ permissions: {} jobs: macos-disk-cleanup: + if: github.repository == 'electron/electron' strategy: fail-fast: false matrix: diff --git a/.github/workflows/macos-publish.yml b/.github/workflows/macos-publish.yml index 7078d23ce3..22181d98a3 100644 --- a/.github/workflows/macos-publish.yml +++ b/.github/workflows/macos-publish.yml @@ -22,6 +22,7 @@ permissions: {} jobs: checkout-macos: + if: github.repository == 'electron/electron' runs-on: electron-arc-centralus-linux-amd64-32core permissions: contents: read diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 407a70d6d8..5c1aba9d25 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -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. diff --git a/.github/workflows/stable-prep-items.yml b/.github/workflows/stable-prep-items.yml index c649eba5af..8e9f03156f 100644 --- a/.github/workflows/stable-prep-items.yml +++ b/.github/workflows/stable-prep-items.yml @@ -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: diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index a4e4def4d3..9e899c78e7 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -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 diff --git a/.github/workflows/windows-publish.yml b/.github/workflows/windows-publish.yml index 74b87ac3ff..b761ac86b4 100644 --- a/.github/workflows/windows-publish.yml +++ b/.github/workflows/windows-publish.yml @@ -22,6 +22,7 @@ permissions: {} jobs: checkout-windows: + if: github.repository == 'electron/electron' runs-on: electron-arc-centralus-linux-amd64-32core permissions: contents: read From 4aa89b9c3cd3e1716667372872231a1e2f623dbd Mon Sep 17 00:00:00 2001 From: David Sanders Date: Mon, 26 Jan 2026 11:37:01 -0800 Subject: [PATCH 02/38] ci: add pipeline segment to run clang-tidy (#49072) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ci: add pipeline segment to run clang-tidy * chore: set -header-filter='' for clang-tidy Breaking change in `clang-tidy` 22.0.0: > clang-tidy now displays warnings from all non-system headers by default. > Previously, users had to explicitly opt-in to header warnings using > -header-filter=’.*’. To disable warnings from non-system, set > -header-filter to an empty string. --- .github/actions/build-electron/action.yml | 9 + .github/workflows/build.yml | 4 +- ...ectron-build-and-tidy-and-test-and-nan.yml | 124 ++++++++++++++ ...eline-electron-build-and-tidy-and-test.yml | 121 +++++++++++++ .../pipeline-segment-electron-build.yml | 6 + .../pipeline-segment-electron-clang-tidy.yml | 159 ++++++++++++++++++ script/run-clang-tidy.ts | 2 +- 7 files changed, 423 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/pipeline-electron-build-and-tidy-and-test-and-nan.yml create mode 100644 .github/workflows/pipeline-electron-build-and-tidy-and-test.yml create mode 100644 .github/workflows/pipeline-segment-electron-clang-tidy.yml diff --git a/.github/actions/build-electron/action.yml b/.github/actions/build-electron/action.yml index 26e79025f4..ba5bef4f3f 100644 --- a/.github/actions/build-electron/action.yml +++ b/.github/actions/build-electron/action.yml @@ -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: @@ -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 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2ee7847b42..a039b74a24 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -284,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 diff --git a/.github/workflows/pipeline-electron-build-and-tidy-and-test-and-nan.yml b/.github/workflows/pipeline-electron-build-and-tidy-and-test-and-nan.yml new file mode 100644 index 0000000000..2cbe33ec7b --- /dev/null +++ b/.github/workflows/pipeline-electron-build-and-tidy-and-test-and-nan.yml @@ -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 diff --git a/.github/workflows/pipeline-electron-build-and-tidy-and-test.yml b/.github/workflows/pipeline-electron-build-and-tidy-and-test.yml new file mode 100644 index 0000000000..103aafaa04 --- /dev/null +++ b/.github/workflows/pipeline-electron-build-and-tidy-and-test.yml @@ -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 diff --git a/.github/workflows/pipeline-segment-electron-build.yml b/.github/workflows/pipeline-segment-electron-build.yml index 2d46f54d3a..278834a448 100644 --- a/.github/workflows/pipeline-segment-electron-build.yml +++ b/.github/workflows/pipeline-segment-electron-build.yml @@ -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 @@ -201,6 +206,7 @@ jobs: 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: | diff --git a/.github/workflows/pipeline-segment-electron-clang-tidy.yml b/.github/workflows/pipeline-segment-electron-clang-tidy.yml new file mode 100644 index 0000000000..90b42ecc37 --- /dev/null +++ b/.github/workflows/pipeline-segment-electron-clang-tidy.yml @@ -0,0 +1,159 @@ +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: ${{ 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: ${{ inputs.target-platform == 'macos' && 'darwin' || inputs.target-platform }} + TARGET_ARCH: ${{ inputs.target-arch }} + TARGET_PLATFORM: ${{ inputs.target-platform }} + ARTIFACT_KEY: ${{ inputs.target-platform == 'macos' && 'darwin' || inputs.target-platform }}_${{ inputs.target-arch }} + steps: + - name: Checkout Electron + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 + 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 diff --git a/script/run-clang-tidy.ts b/script/run-clang-tidy.ts index ea8a0a0bf7..ab3ad9efe8 100644 --- a/script/run-clang-tidy.ts +++ b/script/run-clang-tidy.ts @@ -118,7 +118,7 @@ async function runClangTidy ( fix: boolean = false ): Promise { 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'); From 0cc15a6386f192691f74a65dcee4a2304a375fc1 Mon Sep 17 00:00:00 2001 From: David Sanders Date: Mon, 26 Jan 2026 12:13:34 -0800 Subject: [PATCH 03/38] ci: reapply patches if PR base branch updates them (#49516) --- .github/workflows/build.yml | 39 +++++++++++++ .github/workflows/rerun-apply-patches.yml | 69 +++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 .github/workflows/rerun-apply-patches.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a039b74a24..b9d0e9e0b9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -57,6 +57,7 @@ jobs: src: ${{ steps.filter.outputs.src }} build-image-sha: ${{ steps.set-output.outputs.build-image-sha }} docs-only: ${{ steps.set-output.outputs.docs-only }} + has-patches: ${{ steps.filter.outputs.patches }} steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 with: @@ -73,6 +74,9 @@ jobs: - CODE_OF_CONDUCT.md src: - '!docs/**' + patches: + - DEPS + - 'patches/**' - name: Set Outputs for Build Image SHA & Docs Only id: set-output run: | @@ -105,6 +109,41 @@ jobs: container: '{"image":"ghcr.io/electron/build:${{ needs.checkout-linux.outputs.build-image-sha }}","options":"--user root","volumes":["/mnt/cross-instance-cache:/mnt/cross-instance-cache"]}' secrets: inherit + # Apply Patches Job + apply-patches: + needs: setup + if: ${{ needs.setup.outputs.has-patches == 'true' }} + runs-on: electron-arc-centralus-linux-amd64-32core + permissions: + contents: read + container: + image: ghcr.io/electron/build:${{ needs.setup.outputs.build-image-sha }} + options: --user root + volumes: + - /mnt/cross-instance-cache:/mnt/cross-instance-cache + - /var/run/sas:/var/run/sas + env: + CHROMIUM_GIT_COOKIE: ${{ secrets.CHROMIUM_GIT_COOKIE }} + GCLIENT_EXTRA_ARGS: '--custom-var=checkout_arm=True --custom-var=checkout_arm64=True' + steps: + - name: Checkout Electron + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + with: + path: src/electron + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} + - name: Rebase onto Base Branch + working-directory: src/electron + run: | + git config user.email "electron@github.com" + git config user.name "Electron Bot" + git fetch origin ${{ github.event.pull_request.base.ref }} + git rebase origin/${{ github.event.pull_request.base.ref }} + - name: Checkout & Sync & Save + uses: ./src/electron/.github/actions/checkout + with: + target-platform: linux + # Checkout Jobs checkout-macos: needs: setup diff --git a/.github/workflows/rerun-apply-patches.yml b/.github/workflows/rerun-apply-patches.yml new file mode 100644 index 0000000000..cdf689ce70 --- /dev/null +++ b/.github/workflows/rerun-apply-patches.yml @@ -0,0 +1,69 @@ +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: + contents: read + actions: write + 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 job check for this PR + CHECK=$(gh pr checks "$PR_NUMBER" --json link,name,state,workflow --jq '[.[] | select(.workflow == "Build" and .name == "apply-patches")] | first') + + if [ -z "$CHECK" ] || [ "$CHECK" = "null" ]; then + echo " No apply-patches job found for PR #${PR_NUMBER}" + continue + fi + + STATE=$(echo "$CHECK" | jq -r '.state') + if [ "$STATE" = "SKIPPED" ]; then + echo " apply-patches job was skipped for PR #${PR_NUMBER} (no patches)" + continue + fi + + LINK=$(echo "$CHECK" | jq -r '.link') + + # 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 + + # Get the job database ID for the apply-patches job + JOB_ID=$(gh run view "$RUN_ID" --json jobs --jq '.jobs[] | select(.name == "apply-patches") | .databaseId') + + if [ -z "$JOB_ID" ]; then + echo " Could not find apply-patches job ID for run: ${RUN_ID}" + continue + fi + + gh run rerun "$RUN_ID" --job "$JOB_ID" + done From ec5eb6478806f2a90fc97950c96788386a90094f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:00:13 +0100 Subject: [PATCH 04/38] build(deps): bump github/codeql-action from 4.31.10 to 4.32.0 (#49540) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.31.10 to 4.32.0. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/cdefb33c0f6224e58673d9004f47f7cb3e328b89...b20883b0cd1f46c72ae0ba6d1090936928f9fa30) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.32.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/scorecards.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 5c1aba9d25..5143485c84 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -51,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@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v3.29.5 with: sarif_file: results.sarif From b2c50935424f99337c164893c2149b78c8e675ec Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Tue, 27 Jan 2026 11:02:51 +0100 Subject: [PATCH 05/38] docs: correct type for `process.noDeprecation` (#49524) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: correct type for process.noDeprecation * docs: mark `noDeprecation` as optional instead Co-authored-by: René --------- Co-authored-by: David Sanders Co-authored-by: René --- docs/api/process.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/process.md b/docs/api/process.md index 91e42942ff..78685084d2 100644 --- a/docs/api/process.md +++ b/docs/api/process.md @@ -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. From c521c2b86824726f990f5516f3d5125a9ef14ac2 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Tue, 27 Jan 2026 11:03:09 +0100 Subject: [PATCH 06/38] test: update/fix Node.js disabled specs (#49467) --- .../fix_crypto_tests_to_run_with_bssl.patch | 823 ++++++++++++++++-- script/node-disabled-tests.json | 89 +- 2 files changed, 738 insertions(+), 174 deletions(-) diff --git a/patches/node/fix_crypto_tests_to_run_with_bssl.patch b/patches/node/fix_crypto_tests_to_run_with_bssl.patch index d76b03d3fa..ed15a7a861 100644 --- a/patches/node/fix_crypto_tests_to_run_with_bssl.patch +++ b/patches/node/fix_crypto_tests_to_run_with_bssl.patch @@ -10,18 +10,6 @@ This should be upstreamed in some form, though it may need to be tweaked before it's acceptable to upstream, as this patch comments out a couple of tests that upstream probably cares about. -diff --git a/test/fixtures/crypto/rsa_pss.js b/test/fixtures/crypto/rsa_pss.js -index 423f2c4d77bfc98bfbdab93c09aff8012c678cbd..fa0bcceb5697486930a9530732f9a9ab6e1bb5b0 100644 ---- a/test/fixtures/crypto/rsa_pss.js -+++ b/test/fixtures/crypto/rsa_pss.js -@@ -1,6 +1,6 @@ - 'use strict'; - --module.exports = function() { -+module.exports = function () { - const pkcs8 = Buffer.from( - '308204bf020100300d06092a864886f70d0101010500048204a9308204a5020100028' + - '2010100d3576092e62957364544e7e4233b7bdb293db2085122c479328546f9f0f712' + diff --git a/test/parallel/test-crypto-async-sign-verify.js b/test/parallel/test-crypto-async-sign-verify.js index 9876c4bb6ecd2e5b8879f153811cd0a0a22997aa..2c4bf03452eb10fec52c38a361b6aad93169f08d 100644 --- a/test/parallel/test-crypto-async-sign-verify.js @@ -53,6 +41,102 @@ index 9876c4bb6ecd2e5b8879f153811cd0a0a22997aa..2c4bf03452eb10fec52c38a361b6aad9 // Test Parallel Execution w/ KeyObject is threadsafe in openssl3 { +diff --git a/test/parallel/test-crypto-authenticated.js b/test/parallel/test-crypto-authenticated.js +index e8fedf2d5d5072e00afd493ac2ac44748212b02e..6fcbe244871d25b2151d39160149aaa50dc96012 100644 +--- a/test/parallel/test-crypto-authenticated.js ++++ b/test/parallel/test-crypto-authenticated.js +@@ -627,21 +627,25 @@ for (const test of TEST_CASES) { + { + // CCM cipher without data should not crash, see https://github.com/nodejs/node/issues/38035. + const algo = 'aes-128-ccm'; +- const key = Buffer.alloc(16); +- const iv = Buffer.alloc(12); +- const opts = { authTagLength: 10 }; ++ if (!ciphers.includes(algo)) { ++ common.printSkipMessage(`unsupported ${algo} test`); ++ } else { ++ const key = Buffer.alloc(16); ++ const iv = Buffer.alloc(12); ++ const opts = { authTagLength: 10 }; + +- const cipher = crypto.createCipheriv(algo, key, iv, opts); +- assert.throws(() => { +- cipher.final(); +- }, hasOpenSSL3 ? { +- code: 'ERR_OSSL_TAG_NOT_SET' +- } : { +- message: /Unsupported state/ +- }); ++ const cipher = crypto.createCipheriv(algo, key, iv, opts); ++ assert.throws(() => { ++ cipher.final(); ++ }, hasOpenSSL3 ? { ++ code: 'ERR_OSSL_TAG_NOT_SET' ++ } : { ++ message: /Unsupported state/ ++ }); ++ } + } + +-{ ++if (!process.features.openssl_is_boringssl) { + const key = Buffer.alloc(32); + const iv = Buffer.alloc(12); + +@@ -653,11 +657,13 @@ for (const test of TEST_CASES) { + message: errMessages.authTagLength + }); + } ++} else { ++ common.printSkipMessage('Skipping unsupported chacha20-poly1305 test'); + } + + // ChaCha20-Poly1305 should respect the authTagLength option and should not + // require the authentication tag before calls to update() during decryption. +-{ ++if (!process.features.openssl_is_boringssl) { + const key = Buffer.alloc(32); + const iv = Buffer.alloc(12); + +@@ -697,6 +703,8 @@ for (const test of TEST_CASES) { + } + } + } ++} else { ++ common.printSkipMessage('Skipping unsupported chacha20-poly1305 test'); + } + + // ChaCha20-Poly1305 should default to an authTagLength of 16. When encrypting, +@@ -706,7 +714,7 @@ for (const test of TEST_CASES) { + // shorter tags as long as their length was valid according to NIST SP 800-38D. + // For ChaCha20-Poly1305, we intentionally deviate from that because there are + // no recommended or approved authentication tag lengths below 16 bytes. +-{ ++if (!process.features.openssl_is_boringssl) { + const rfcTestCases = TEST_CASES.filter(({ algo, tampered }) => { + return algo === 'chacha20-poly1305' && tampered === false; + }); +@@ -740,10 +748,12 @@ for (const test of TEST_CASES) { + + assert.strictEqual(plaintext.toString('hex'), testCase.plain); + } ++} else { ++ common.printSkipMessage('Skipping unsupported chacha20-poly1305 test'); + } + + // https://github.com/nodejs/node/issues/45874 +-{ ++if (!process.features.openssl_is_boringssl) { + const rfcTestCases = TEST_CASES.filter(({ algo, tampered }) => { + return algo === 'chacha20-poly1305' && tampered === false; + }); +@@ -771,4 +781,6 @@ for (const test of TEST_CASES) { + assert.throws(() => { + decipher.final(); + }, /Unsupported state or unable to authenticate data/); ++} else { ++ common.printSkipMessage('Skipping unsupported chacha20-poly1305 test'); + } diff --git a/test/parallel/test-crypto-cipheriv-decipheriv.js b/test/parallel/test-crypto-cipheriv-decipheriv.js index 6742722f9e90914b4dc8c079426d10040d476f72..8801ddfe7023fd0f7d5657b86a9164d75765322e 100644 --- a/test/parallel/test-crypto-cipheriv-decipheriv.js @@ -68,6 +152,21 @@ index 6742722f9e90914b4dc8c079426d10040d476f72..8801ddfe7023fd0f7d5657b86a9164d7 // Test encryption and decryption with explicit key and iv. // AES Key Wrap test vector comes from RFC3394 const plaintext = Buffer.from('00112233445566778899AABBCCDDEEFF', 'hex'); +diff --git a/test/parallel/test-crypto-default-shake-lengths-oneshot.js b/test/parallel/test-crypto-default-shake-lengths-oneshot.js +index 247e58d93c4303ffde132e49fb25cf88d76fae7c..de1648d97c2189c2eb8a6509b19b0c462c203453 100644 +--- a/test/parallel/test-crypto-default-shake-lengths-oneshot.js ++++ b/test/parallel/test-crypto-default-shake-lengths-oneshot.js +@@ -5,6 +5,10 @@ const common = require('../common'); + if (!common.hasCrypto) + common.skip('missing crypto'); + ++if (process.features.openssl_is_boringssl) { ++ common.skip('Skipping unsupported shake128 digest method test'); ++} ++ + const { hash } = require('crypto'); + + common.expectWarning({ diff --git a/test/parallel/test-crypto-dh-curves.js b/test/parallel/test-crypto-dh-curves.js index 81a469c226c261564dee1e0b06b6571b18a41f1f..58b66045dba4201b7ebedd78b129420ffc316051 100644 --- a/test/parallel/test-crypto-dh-curves.js @@ -82,18 +181,71 @@ index 81a469c226c261564dee1e0b06b6571b18a41f1f..58b66045dba4201b7ebedd78b129420f const availableCurves = new Set(crypto.getCurves()); diff --git a/test/parallel/test-crypto-dh-errors.js b/test/parallel/test-crypto-dh-errors.js -index d7527d82617efccd931f0fc2f700ab876872c1e6..b14b4bbf88b902b6de916b92e3d48335c01df911 100644 +index d7527d82617efccd931f0fc2f700ab876872c1e6..5474d094c7af1bec1e9d144e04663a41def9df3c 100644 --- a/test/parallel/test-crypto-dh-errors.js +++ b/test/parallel/test-crypto-dh-errors.js -@@ -27,7 +27,7 @@ assert.throws(() => crypto.createDiffieHellman('abcdef', 13.37), { +@@ -27,13 +27,13 @@ assert.throws(() => crypto.createDiffieHellman('abcdef', 13.37), { for (const bits of [-1, 0, 1]) { if (hasOpenSSL3) { assert.throws(() => crypto.createDiffieHellman(bits), { - code: 'ERR_OSSL_DH_MODULUS_TOO_SMALL', -+ code: 'ERR_OSSL_BN_BITS_TOO_SMALL', ++ code: /ERR_OSSL_(BN_BITS|DH_MODULUS)_TOO_SMALL/, name: 'Error', message: /modulus too small/, }); + } else { + assert.throws(() => crypto.createDiffieHellman(bits), { +- code: 'ERR_OSSL_BN_BITS_TOO_SMALL', ++ code: /ERR_OSSL_(BN_BITS|DH_MODULUS)_TOO_SMALL/, + name: 'Error', + message: /bits[\s_]too[\s_]small/i, + }); +diff --git a/test/parallel/test-crypto-dh-group-setters.js b/test/parallel/test-crypto-dh-group-setters.js +index 7c774111952eada92c62d45674c0845667ead1bf..37d0a44d0e1e102e5a9893cd8e48967050407c76 100644 +--- a/test/parallel/test-crypto-dh-group-setters.js ++++ b/test/parallel/test-crypto-dh-group-setters.js +@@ -6,6 +6,10 @@ if (!common.hasCrypto) + const assert = require('assert'); + const crypto = require('crypto'); + ++if (process.features.openssl_is_boringssl) { ++ common.skip('Skipping unsupported Diffie-Hellman tests'); ++} ++ + // Unlike DiffieHellman, DiffieHellmanGroup does not have any setters. + const dhg = crypto.getDiffieHellman('modp1'); + assert.strictEqual(dhg.constructor, crypto.DiffieHellmanGroup); +diff --git a/test/parallel/test-crypto-dh-modp2-views.js b/test/parallel/test-crypto-dh-modp2-views.js +index 8d01731af79394cb33477a1ba4bb13561604e5e5..a28e615b7f35c7f4fc6ec6f7b065505336e6f832 100644 +--- a/test/parallel/test-crypto-dh-modp2-views.js ++++ b/test/parallel/test-crypto-dh-modp2-views.js +@@ -7,6 +7,10 @@ const assert = require('assert'); + const crypto = require('crypto'); + const { modp2buf } = require('../common/crypto'); + ++if (process.features.openssl_is_boringssl) { ++ common.skip('Skipping unsupported Diffie-Hellman tests'); ++} ++ + const modp2 = crypto.createDiffieHellmanGroup('modp2'); + + const views = common.getArrayBufferViews(modp2buf); +diff --git a/test/parallel/test-crypto-dh-modp2.js b/test/parallel/test-crypto-dh-modp2.js +index 19767d26f4e5fbd1d82b5bfa6ebe0afddc412c3e..eb262f235ff30bf5dc988c1b34052c9856f4d186 100644 +--- a/test/parallel/test-crypto-dh-modp2.js ++++ b/test/parallel/test-crypto-dh-modp2.js +@@ -6,6 +6,11 @@ if (!common.hasCrypto) + const assert = require('assert'); + const crypto = require('crypto'); + const { modp2buf } = require('../common/crypto'); ++ ++if (process.features.openssl_is_boringssl) { ++ common.skip('Skipping unsupported Diffie-Hellman tests'); ++} ++ + const modp2 = crypto.createDiffieHellmanGroup('modp2'); + + { diff --git a/test/parallel/test-crypto-dh.js b/test/parallel/test-crypto-dh.js index 3c00a5fc73bb9f86f944df74f29d6b5225bc2f0e..b4e7002d862907d2af3b4f8e985700bd03300809 100644 --- a/test/parallel/test-crypto-dh.js @@ -146,6 +298,234 @@ index d22281abbd5c3cab3aaa3ac494301fa6b4a8a968..5f0c6a4aed2e868a1a1049212edf2187 s.pipe(h).on('data', common.mustCall(function(c) { assert.strictEqual(c, expect); +diff --git a/test/parallel/test-crypto-key-objects-to-crypto-key.js b/test/parallel/test-crypto-key-objects-to-crypto-key.js +index 141e51d1ab74a4fc3b176b303807fb1cf2a58ce1..7ea6643fe5c8cc0e7613782419e1d465f99314cd 100644 +--- a/test/parallel/test-crypto-key-objects-to-crypto-key.js ++++ b/test/parallel/test-crypto-key-objects-to-crypto-key.js +@@ -26,9 +26,14 @@ function assertCryptoKey(cryptoKey, keyObject, algorithm, extractable, usages) { + { + for (const length of [128, 192, 256]) { + const key = createSecretKey(randomBytes(length >> 3)); +- const algorithms = ['AES-CTR', 'AES-CBC', 'AES-GCM', 'AES-KW']; ++ let algorithms = ['AES-CTR', 'AES-CBC', 'AES-GCM', 'AES-KW']; + if (length === 256) + algorithms.push('ChaCha20-Poly1305'); ++ ++ if (process.features.openssl_is_boringssl) { ++ algorithms = algorithms.filter((a) => a !== 'AES-KW' && a !== 'ChaCha20-Poly1305'); ++ } ++ + for (const algorithm of algorithms) { + const usages = algorithm === 'AES-KW' ? ['wrapKey', 'unwrapKey'] : ['encrypt', 'decrypt']; + for (const extractable of [true, false]) { +@@ -97,7 +102,14 @@ function assertCryptoKey(cryptoKey, keyObject, algorithm, extractable, usages) { + } + + { +- for (const algorithm of ['Ed25519', 'Ed448', 'X25519', 'X448']) { ++ const algorithms = ['Ed25519', 'X25519']; ++ ++ if (!process.features.openssl_is_boringssl) { ++ algorithms.push('X448', 'Ed448'); ++ } ++ ++ for (const algorithm of algorithms) { ++ console.log(algorithm); + const { publicKey, privateKey } = generateKeyPairSync(algorithm.toLowerCase()); + assert.throws(() => { + publicKey.toCryptoKey(algorithm === 'Ed25519' ? 'X25519' : 'Ed25519', true, []); +diff --git a/test/parallel/test-crypto-key-objects.js b/test/parallel/test-crypto-key-objects.js +index e8359ed6d0362c6e8da8be08b0fd42245fa7ae47..bd8211d98261a1acc928e849bf713578c85ff877 100644 +--- a/test/parallel/test-crypto-key-objects.js ++++ b/test/parallel/test-crypto-key-objects.js +@@ -302,11 +302,11 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', + }, hasOpenSSL3 ? { + message: 'error:1E08010C:DECODER routines::unsupported', + } : { +- message: 'error:0909006C:PEM routines:get_name:no start line', ++ message: /no.start.line/i, + code: 'ERR_OSSL_PEM_NO_START_LINE', +- reason: 'no start line', ++ reason: /no.start.line/i, + library: 'PEM routines', +- function: 'get_name', ++ function: /get_name|OPENSSL_internal/, + }); + + // This should not abort either: https://github.com/nodejs/node/issues/29904 +@@ -329,12 +329,12 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', + message: /error:1E08010C:DECODER routines::unsupported/, + library: 'DECODER routines' + } : { +- message: /asn1 encoding/, +- library: 'asn1 encoding routines' ++ message: /asn1 encoding|public key routines/, ++ library: /asn1 encoding routines|public key routines/ + }); + } + +-[ ++const infos = [ + { private: fixtures.readKey('ed25519_private.pem', 'ascii'), + public: fixtures.readKey('ed25519_public.pem', 'ascii'), + keyType: 'ed25519', +@@ -344,17 +344,6 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', + d: 'wVK6M3SMhQh3NK-7GRrSV-BVWQx1FO5pW8hhQeu_NdA', + kty: 'OKP' + } }, +- { private: fixtures.readKey('ed448_private.pem', 'ascii'), +- public: fixtures.readKey('ed448_public.pem', 'ascii'), +- keyType: 'ed448', +- jwk: { +- crv: 'Ed448', +- x: 'oX_ee5-jlcU53-BbGRsGIzly0V-SZtJ_oGXY0udf84q2hTW2RdstLktvwpkVJOoNb7o' + +- 'Dgc2V5ZUA', +- d: '060Ke71sN0GpIc01nnGgMDkp0sFNQ09woVo4AM1ffax1-mjnakK0-p-S7-Xf859QewX' + +- 'jcR9mxppY', +- kty: 'OKP' +- } }, + { private: fixtures.readKey('x25519_private.pem', 'ascii'), + public: fixtures.readKey('x25519_public.pem', 'ascii'), + keyType: 'x25519', +@@ -364,18 +353,37 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', + d: 'mL_IWm55RrALUGRfJYzw40gEYWMvtRkesP9mj8o8Omc', + kty: 'OKP' + } }, +- { private: fixtures.readKey('x448_private.pem', 'ascii'), ++] ++ ++if (!process.features.openssl_is_boringssl) { ++ infos.push({ ++ private: fixtures.readKey('ed448_private.pem', 'ascii'), ++ public: fixtures.readKey('ed448_public.pem', 'ascii'), ++ keyType: 'ed448', ++ jwk: { ++ crv: 'Ed448', ++ x: 'oX_ee5-jlcU53-BbGRsGIzly0V-SZtJ_oGXY0udf84q2hTW2RdstLktvwpkVJOoNb7o' + ++ 'Dgc2V5ZUA', ++ d: '060Ke71sN0GpIc01nnGgMDkp0sFNQ09woVo4AM1ffax1-mjnakK0-p-S7-Xf859QewX' + ++ 'jcR9mxppY', ++ kty: 'OKP' ++ } ++ }, { ++ private: fixtures.readKey('x448_private.pem', 'ascii'), + public: fixtures.readKey('x448_public.pem', 'ascii'), + keyType: 'x448', + jwk: { + crv: 'X448', + x: 'ioHSHVpTs6hMvghosEJDIR7ceFiE3-Xccxati64oOVJ7NWjfozE7ae31PXIUFq6cVYg' + +- 'vSKsDFPA', ++ 'vSKsDFPA', + d: 'tMNtrO_q8dlY6Y4NDeSTxNQ5CACkHiPvmukidPnNIuX_EkcryLEXt_7i6j6YZMKsrWy' + +- 'S0jlSYJk', ++ 'S0jlSYJk', + kty: 'OKP' +- } }, +-].forEach((info) => { ++ } ++ }); ++} ++ ++infos.forEach((info) => { + const keyType = info.keyType; + + { +@@ -417,7 +425,7 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', + } + }); + +-[ ++const ecInfos = [ + { private: fixtures.readKey('ec_p256_private.pem', 'ascii'), + public: fixtures.readKey('ec_p256_public.pem', 'ascii'), + keyType: 'ec', +@@ -429,17 +437,6 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', + x: 'X0mMYR_uleZSIPjNztIkAS3_ud5LhNpbiIFp6fNf2Gs', + y: 'UbJuPy2Xi0lW7UYTBxPK3yGgDu9EAKYIecjkHX5s2lI' + } }, +- { private: fixtures.readKey('ec_secp256k1_private.pem', 'ascii'), +- public: fixtures.readKey('ec_secp256k1_public.pem', 'ascii'), +- keyType: 'ec', +- namedCurve: 'secp256k1', +- jwk: { +- crv: 'secp256k1', +- d: 'c34ocwTwpFa9NZZh3l88qXyrkoYSxvC0FEsU5v1v4IM', +- kty: 'EC', +- x: 'cOzhFSpWxhalCbWNdP2H_yUkdC81C9T2deDpfxK7owA', +- y: '-A3DAZTk9IPppN-f03JydgHaFvL1fAHaoXf4SX4NXyo' +- } }, + { private: fixtures.readKey('ec_p384_private.pem', 'ascii'), + public: fixtures.readKey('ec_p384_public.pem', 'ascii'), + keyType: 'ec', +@@ -465,7 +462,25 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', + y: 'Ad3flexBeAfXceNzRBH128kFbOWD6W41NjwKRqqIF26vmgW_8COldGKZjFkOSEASxPB' + + 'cvA2iFJRUyQ3whC00j0Np' + } }, +-].forEach((info) => { ++] ++ ++if (!process.features.openssl_is_boringssl) { ++ ecInfos.push({ ++ private: fixtures.readKey('ec_secp256k1_private.pem', 'ascii'), ++ public: fixtures.readKey('ec_secp256k1_public.pem', 'ascii'), ++ keyType: 'ec', ++ namedCurve: 'secp256k1', ++ jwk: { ++ crv: 'secp256k1', ++ d: 'c34ocwTwpFa9NZZh3l88qXyrkoYSxvC0FEsU5v1v4IM', ++ kty: 'EC', ++ x: 'cOzhFSpWxhalCbWNdP2H_yUkdC81C9T2deDpfxK7owA', ++ y: '-A3DAZTk9IPppN-f03JydgHaFvL1fAHaoXf4SX4NXyo' ++ } ++ }); ++} ++ ++ecInfos.forEach((info) => { + const { keyType, namedCurve } = info; + + { +@@ -540,7 +555,7 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', + format: 'pem', + passphrase: Buffer.alloc(1024, 'a') + }), { +- message: /bad decrypt/ ++ message: /bad.decrypt/i + }); + + const publicKey = createPublicKey(publicDsa); +@@ -566,7 +581,7 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', + + { + // Test RSA-PSS. +- { ++ if (!process.features.openssl_is_boringssl) { + // This key pair does not restrict the message digest algorithm or salt + // length. + const publicPem = fixtures.readKey('rsa_pss_public_2048.pem'); +@@ -625,6 +640,8 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', + }, { + code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' + }); ++ } else { ++ common.skip('Skipping unsupported RSA-PSS key test'); + } + + { +diff --git a/test/parallel/test-crypto-keygen-deprecation.js b/test/parallel/test-crypto-keygen-deprecation.js +index 926dfbbc4ae987217ab404ec25a3ca0a2ef2edcf..df0b379c1b1e982b96ea97c9814f38991d734ce4 100644 +--- a/test/parallel/test-crypto-keygen-deprecation.js ++++ b/test/parallel/test-crypto-keygen-deprecation.js +@@ -4,6 +4,10 @@ const common = require('../common'); + if (!common.hasCrypto) + common.skip('missing crypto'); + ++if (process.features.openssl_is_boringssl) { ++ common.skip('Skipping unsupported RSA-PSS key tests'); ++} ++ + const DeprecationWarning = []; + DeprecationWarning.push([ + '"options.hash" is deprecated, use "options.hashAlgorithm" instead.', diff --git a/test/parallel/test-crypto-oneshot-hash-xof.js b/test/parallel/test-crypto-oneshot-hash-xof.js index 75cb4800ff1bd51fedd7bc4e2d7e6af6f4f48346..b4363c31592763235116d970a5f45d4cf63de373 100644 --- a/test/parallel/test-crypto-oneshot-hash-xof.js @@ -162,7 +542,7 @@ index 75cb4800ff1bd51fedd7bc4e2d7e6af6f4f48346..b4363c31592763235116d970a5f45d4c { // Default outputLengths. diff --git a/test/parallel/test-crypto-rsa-dsa.js b/test/parallel/test-crypto-rsa-dsa.js -index 119bc3c2d20ea7d681f0b579f9d91ad46cdc3634..8d13b105fa426015a873c411ad1d7f64b3d9580e 100644 +index 119bc3c2d20ea7d681f0b579f9d91ad46cdc3634..ad9cd4fd81aff32ec175f469176e1012b81872ac 100644 --- a/test/parallel/test-crypto-rsa-dsa.js +++ b/test/parallel/test-crypto-rsa-dsa.js @@ -29,12 +29,11 @@ const dsaPkcs8KeyPem = fixtures.readKey('dsa_private_pkcs8.pem'); @@ -175,24 +555,29 @@ index 119bc3c2d20ea7d681f0b579f9d91ad46cdc3634..8d13b105fa426015a873c411ad1d7f64 - reason: 'bad decrypt', - function: 'EVP_DecryptFinal_ex', - library: 'digital envelope routines', -+ message: /error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt|error:1e000065:Cipher functions:OPENSSL_internal:BAD_DECRYPT/, -+ code: /ERR_OSSL(_EVP)?_BAD_DECRYPT/, -+ reason: /bad decrypt|BAD_DECRYPT/, ++ message: /bad decrypt|BAD_DECRYPT/i, ++ code: /ERR_OSSL_(EVP_)?BAD_DECRYPT/, ++ reason: /bad decrypt|BAD_DECRYPT/i, + function: /EVP_DecryptFinal_ex|OPENSSL_internal/, + library: /digital envelope routines|Cipher functions/, }; const decryptError = hasOpenSSL3 ? -@@ -223,7 +222,7 @@ function test_rsa(padding, encryptOaepHash, decryptOaepHash) { - }, bufferToEncrypt); +@@ -325,9 +324,12 @@ function test_rsa(padding, encryptOaepHash, decryptOaepHash) { + } + test_rsa('RSA_NO_PADDING'); +-test_rsa('RSA_PKCS1_PADDING'); + test_rsa('RSA_PKCS1_OAEP_PADDING'); -- if (padding === constants.RSA_PKCS1_PADDING) { -+ if (!process.features.openssl_is_boringssl) { - if (!process.config.variables.node_shared_openssl) { - // TODO(richardlau) remove check and else branch after deps/openssl - // is upgraded. -@@ -489,7 +488,7 @@ assert.throws(() => { ++if (!process.features.openssl_is_boringssl) { ++ test_rsa('RSA_PKCS1_PADDING'); ++} ++ + // Test OAEP with different hash functions. + test_rsa('RSA_PKCS1_OAEP_PADDING', undefined, 'sha1'); + test_rsa('RSA_PKCS1_OAEP_PADDING', 'sha1', undefined); +@@ -489,7 +491,7 @@ assert.throws(() => { // // Test DSA signing and verification // @@ -201,6 +586,48 @@ index 119bc3c2d20ea7d681f0b579f9d91ad46cdc3634..8d13b105fa426015a873c411ad1d7f64 const input = 'I AM THE WALRUS'; // DSA signatures vary across runs so there is no static string to verify +@@ -512,13 +514,15 @@ assert.throws(() => { + verify2.update(input); + + assert.strictEqual(verify2.verify(dsaPubPem, signature2, 'hex'), true); ++} else { ++ common.printSkipMessage('Skipping unsupported DSA test case'); + } + + + // + // Test DSA signing and verification with PKCS#8 private key + // +-{ ++if (!process.features.openssl_is_boringssl) { + const input = 'I AM THE WALRUS'; + + // DSA signatures vary across runs so there is no static string to verify +@@ -531,6 +535,8 @@ assert.throws(() => { + verify.update(input); + + assert.strictEqual(verify.verify(dsaPubPem, signature, 'hex'), true); ++} else { ++ common.printSkipMessage('Skipping unsupported DSA test case'); + } + + +@@ -547,7 +553,7 @@ const input = 'I AM THE WALRUS'; + }, decryptPrivateKeyError); + } + +-{ ++if (!process.features.openssl_is_boringssl) { + // DSA signatures vary across runs so there is no static string to verify + // against. + const sign = crypto.createSign('SHA1'); +@@ -559,4 +565,6 @@ const input = 'I AM THE WALRUS'; + verify.update(input); + + assert.strictEqual(verify.verify(dsaPubPem, signature, 'hex'), true); ++} else { ++ common.printSkipMessage('Skipping unsupported DSA test case'); + } diff --git a/test/parallel/test-crypto-scrypt.js b/test/parallel/test-crypto-scrypt.js index eafdfe392bde8eb1fde1dc7e7e9ae51682c74b87..2907e0175379266c90acb9df829d10283bd46652 100644 --- a/test/parallel/test-crypto-scrypt.js @@ -273,7 +700,7 @@ index a66f0a94efd7c952c1d2320fbc7a39fe3a88a8a1..dc5846db0e3dcf8f7cb5f7efcdbc81c1 for (const [file, length] of keys) { const privKey = fixtures.readKey(file); diff --git a/test/parallel/test-crypto.js b/test/parallel/test-crypto.js -index 84111740cd9ef6425b747e24e984e66e46b0b2ef..b1621d310536fae3fdec91a6a9d275ec8fc99a98 100644 +index 84111740cd9ef6425b747e24e984e66e46b0b2ef..974ee53431ca853aeee1ffbd845e65504feef02e 100644 --- a/test/parallel/test-crypto.js +++ b/test/parallel/test-crypto.js @@ -62,7 +62,7 @@ assert.throws(() => { @@ -312,81 +739,297 @@ index 84111740cd9ef6425b747e24e984e66e46b0b2ef..b1621d310536fae3fdec91a6a9d275ec validateList(crypto.getHashes()); // Make sure all of the hashes are supported by OpenSSL for (const algo of crypto.getHashes()) -@@ -197,6 +195,7 @@ assert.throws( +@@ -197,61 +195,63 @@ assert.throws( } ); +-assert.throws(() => { +- const priv = [ +- '-----BEGIN RSA PRIVATE KEY-----', +- 'MIGrAgEAAiEA+3z+1QNF2/unumadiwEr+C5vfhezsb3hp4jAnCNRpPcCAwEAAQIgQNriSQK4', +- 'EFwczDhMZp2dvbcz7OUUyt36z3S4usFPHSECEQD/41K7SujrstBfoCPzwC1xAhEA+5kt4BJy', +- 'eKN7LggbF3Dk5wIQN6SL+fQ5H/+7NgARsVBp0QIRANxYRukavs4QvuyNhMx+vrkCEQCbf6j/', +- 'Ig6/HueCK/0Jkmp+', +- '-----END RSA PRIVATE KEY-----', +- '', +- ].join('\n'); +- crypto.createSign('SHA256').update('test').sign(priv); +-}, (err) => { +- if (!hasOpenSSL3) +- assert.ok(!('opensslErrorStack' in err)); +- assert.throws(() => { throw err; }, hasOpenSSL3 ? { +- name: 'Error', +- message: 'error:02000070:rsa routines::digest too big for rsa key', +- library: 'rsa routines', +- } : { +- name: 'Error', +- message: /routines:RSA_sign:digest too big for rsa key$/, +- library: /rsa routines/i, +- function: 'RSA_sign', +- reason: /digest[\s_]too[\s_]big[\s_]for[\s_]rsa[\s_]key/i, +- code: 'ERR_OSSL_RSA_DIGEST_TOO_BIG_FOR_RSA_KEY' +- }); +- return true; +-}); +- +-if (!hasOpenSSL3) { +if (!process.features.openssl_is_boringssl) { - assert.throws(() => { - const priv = [ - '-----BEGIN RSA PRIVATE KEY-----', -@@ -253,7 +252,7 @@ if (!hasOpenSSL3) { + assert.throws(() => { +- // The correct header inside `rsa_private_pkcs8_bad.pem` should have been +- // -----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY----- +- // instead of +- // -----BEGIN RSA PRIVATE KEY----- and -----END RSA PRIVATE KEY----- +- const sha1_privateKey = fixtures.readKey('rsa_private_pkcs8_bad.pem', +- 'ascii'); +- // This would inject errors onto OpenSSL's error stack +- crypto.createSign('sha1').sign(sha1_privateKey); ++ const priv = [ ++ '-----BEGIN RSA PRIVATE KEY-----', ++ 'MIGrAgEAAiEA+3z+1QNF2/unumadiwEr+C5vfhezsb3hp4jAnCNRpPcCAwEAAQIgQNriSQK4', ++ 'EFwczDhMZp2dvbcz7OUUyt36z3S4usFPHSECEQD/41K7SujrstBfoCPzwC1xAhEA+5kt4BJy', ++ 'eKN7LggbF3Dk5wIQN6SL+fQ5H/+7NgARsVBp0QIRANxYRukavs4QvuyNhMx+vrkCEQCbf6j/', ++ 'Ig6/HueCK/0Jkmp+', ++ '-----END RSA PRIVATE KEY-----', ++ '', ++ ].join('\n'); ++ crypto.createSign('SHA256').update('test').sign(priv); + }, (err) => { +- // Do the standard checks, but then do some custom checks afterwards. +- assert.throws(() => { throw err; }, { +- message: 'error:0D0680A8:asn1 encoding routines:asn1_check_tlen:' + +- 'wrong tag', +- library: 'asn1 encoding routines', +- function: 'asn1_check_tlen', +- reason: 'wrong tag', +- code: 'ERR_OSSL_ASN1_WRONG_TAG', ++ if (!hasOpenSSL3) ++ assert.ok(!('opensslErrorStack' in err)); ++ assert.throws(() => { throw err; }, hasOpenSSL3 ? { ++ name: 'Error', ++ message: 'error:02000070:rsa routines::digest too big for rsa key', ++ library: 'rsa routines', ++ } : { ++ name: 'Error', ++ message: /routines:RSA_sign:digest too big for rsa key$/, ++ library: /rsa routines/i, ++ function: 'RSA_sign', ++ reason: /digest[\s_]too[\s_]big[\s_]for[\s_]rsa[\s_]key/i, ++ code: 'ERR_OSSL_RSA_DIGEST_TOO_BIG_FOR_RSA_KEY' + }); +- // Throws crypto error, so there is an opensslErrorStack property. +- // The openSSL stack should have content. +- assert(Array.isArray(err.opensslErrorStack)); +- assert(err.opensslErrorStack.length > 0); return true; }); ++ ++ if (!hasOpenSSL3) { ++ assert.throws(() => { ++ // The correct header inside `rsa_private_pkcs8_bad.pem` should have been ++ // -----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY----- ++ // instead of ++ // -----BEGIN RSA PRIVATE KEY----- and -----END RSA PRIVATE KEY----- ++ const sha1_privateKey = fixtures.readKey('rsa_private_pkcs8_bad.pem', ++ 'ascii'); ++ // This would inject errors onto OpenSSL's error stack ++ crypto.createSign('sha1').sign(sha1_privateKey); ++ }, (err) => { ++ // Do the standard checks, but then do some custom checks afterwards. ++ assert.throws(() => { throw err; }, { ++ message: 'error:0D0680A8:asn1 encoding routines:asn1_check_tlen:' + ++ 'wrong tag', ++ library: 'asn1 encoding routines', ++ function: 'asn1_check_tlen', ++ reason: 'wrong tag', ++ code: 'ERR_OSSL_ASN1_WRONG_TAG', ++ }); ++ // Throws crypto error, so there is an opensslErrorStack property. ++ // The openSSL stack should have content. ++ assert(Array.isArray(err.opensslErrorStack)); ++ assert(err.opensslErrorStack.length > 0); ++ return true; ++ }); ++ } } -- -+} - // Make sure memory isn't released before being returned - console.log(crypto.randomBytes(16)); + // Make sure memory isn't released before being returned +diff --git a/test/parallel/test-tls-client-auth.js b/test/parallel/test-tls-client-auth.js +index b347c0a88df571296127985f8e7b70de66726cc0..66465783d344dab1330069e36577d41fc75db962 100644 +--- a/test/parallel/test-tls-client-auth.js ++++ b/test/parallel/test-tls-client-auth.js +@@ -112,7 +112,7 @@ if (tls.DEFAULT_MAX_VERSION === 'TLSv1.3') connect({ + // and sends a fatal Alert to the client that the client discovers there has + // been a fatal error. + pair.client.conn.once('error', common.mustCall((err) => { +- assert.strictEqual(err.code, 'ERR_SSL_TLSV13_ALERT_CERTIFICATE_REQUIRED'); ++ //assert.strictEqual(err.code, 'ERR_SSL_TLSV13_ALERT_CERTIFICATE_REQUIRED'); + cleanup(); + })); + }); +diff --git a/test/parallel/test-tls-peer-certificate.js b/test/parallel/test-tls-peer-certificate.js +index 41e3c883d950e074dffcdd6df888eaf47696039c..304724b564956ff3c38cb42793141ddcc57dfd75 100644 +--- a/test/parallel/test-tls-peer-certificate.js ++++ b/test/parallel/test-tls-peer-certificate.js +@@ -55,7 +55,7 @@ connect({ + assert.strictEqual(peerCert.ca, false); + assert.strictEqual(peerCert.issuerCertificate.ca, true); + assert.strictEqual(peerCert.subject.emailAddress, 'ry@tinyclouds.org'); +- assert.strictEqual(peerCert.serialNumber, '147D36C1C2F74206DE9FAB5F2226D78ADB00A426'); ++ assert.match(peerCert.serialNumber, /147D36C1C2F74206DE9FAB5F2226D78ADB00A426/i); + assert.strictEqual(peerCert.exponent, '0x10001'); + assert.strictEqual(peerCert.bits, 2048); + // The conversion to bits is odd because modulus isn't a buffer, its a hex +@@ -95,7 +95,7 @@ connect({ + + const issuer = peerCert.issuerCertificate; + assert.strictEqual(issuer.issuerCertificate, issuer); +- assert.strictEqual(issuer.serialNumber, '4AB16C8DFD6A7D0D2DFCABDF9C4B0E92C6AD0229'); ++ assert.match(issuer.serialNumber, /4AB16C8DFD6A7D0D2DFCABDF9C4B0E92C6AD0229/i); + + return cleanup(); + }); +@@ -114,7 +114,7 @@ connect({ + + assert.ok(peerCert.issuerCertificate); + assert.strictEqual(peerCert.subject.emailAddress, 'ry@tinyclouds.org'); +- assert.strictEqual(peerCert.serialNumber, '32E8197681DA33185867B52885F678BFDBA51727'); ++ assert.match(peerCert.serialNumber, /32E8197681DA33185867B52885F678BFDBA51727/i); + assert.strictEqual(peerCert.exponent, undefined); + assert.strictEqual(peerCert.pubKey, undefined); + assert.strictEqual(peerCert.modulus, undefined); +@@ -146,7 +146,6 @@ connect({ + + const issuer = peerCert.issuerCertificate; + assert.strictEqual(issuer.issuerCertificate, issuer); +- assert.strictEqual(issuer.serialNumber, '32E8197681DA33185867B52885F678BFDBA51727'); +- ++ assert.match(issuer.serialNumber, /32E8197681DA33185867B52885F678BFDBA51727/i); + return cleanup(); + }); +diff --git a/test/parallel/test-tls-pfx-authorizationerror.js b/test/parallel/test-tls-pfx-authorizationerror.js +index eb705d591ef23a90bd78d52797fd1a58bc84a7dd..da428f1320e9e7bd1683724806a7438ed5aa38cc 100644 +--- a/test/parallel/test-tls-pfx-authorizationerror.js ++++ b/test/parallel/test-tls-pfx-authorizationerror.js +@@ -22,13 +22,13 @@ const server = tls + rejectUnauthorized: false + }, + common.mustCall(function(c) { +- assert.strictEqual(c.getPeerCertificate().serialNumber, +- '147D36C1C2F74206DE9FAB5F2226D78ADB00A426'); ++ assert.match(c.getPeerCertificate().serialNumber, ++ /147D36C1C2F74206DE9FAB5F2226D78ADB00A426/i); + assert.strictEqual(c.authorizationError, null); + c.end(); + }) + ) +- .listen(0, function() { ++ .listen(0, common.mustCall(function() { + const client = tls.connect( + { + port: this.address().port, +@@ -36,16 +36,16 @@ const server = tls + passphrase: 'sample', + rejectUnauthorized: false + }, +- function() { ++ common.mustCall(() => { + for (let i = 0; i < 10; ++i) { + // Calling this repeatedly is a regression test that verifies + // that .getCertificate() does not accidentally decrease the + // reference count of the X509* certificate on the native side. +- assert.strictEqual(client.getCertificate().serialNumber, +- '147D36C1C2F74206DE9FAB5F2226D78ADB00A426'); ++ assert.match(client.getCertificate().serialNumber, ++ /147D36C1C2F74206DE9FAB5F2226D78ADB00A426/i); + } + client.end(); + server.close(); +- } ++ }), + ); +- }); ++ })); +diff --git a/test/parallel/test-tls-set-sigalgs.js b/test/parallel/test-tls-set-sigalgs.js +index 985ca13ba2ac7d58f87c263c7654c4f4087efddf..21c199bdb12739f82a075c4e10e08faf8c587cf4 100644 +--- a/test/parallel/test-tls-set-sigalgs.js ++++ b/test/parallel/test-tls-set-sigalgs.js +@@ -65,13 +65,14 @@ test('RSA-PSS+SHA256:RSA-PSS+SHA512:ECDSA+SHA256', + 'RSA-PSS+SHA256:ECDSA+SHA256', + ['RSA-PSS+SHA256', 'ECDSA+SHA256']); + ++const cerr = process.features.openssl_is_boringssl ? ++ 'ERR_SSL_NO_COMMON_SIGNATURE_ALGORITHMS' : 'ERR_SSL_NO_SHARED_SIGNATURE_ALGORITHMS'; ++ + // Do not have shared sigalgs. + const handshakeErr = hasOpenSSL(3, 2) ? + 'ERR_SSL_SSL/TLS_ALERT_HANDSHAKE_FAILURE' : 'ERR_SSL_SSLV3_ALERT_HANDSHAKE_FAILURE'; + test('RSA-PSS+SHA384', 'ECDSA+SHA256', +- undefined, handshakeErr, +- 'ERR_SSL_NO_SHARED_SIGNATURE_ALGORITHMS'); ++ undefined, handshakeErr, cerr); + + test('RSA-PSS+SHA384:ECDSA+SHA256', 'ECDSA+SHA384:RSA-PSS+SHA256', +- undefined, handshakeErr, +- 'ERR_SSL_NO_SHARED_SIGNATURE_ALGORITHMS'); ++ undefined, handshakeErr, cerr); +\ No newline at end of file +diff --git a/test/parallel/test-webcrypto-export-import-cfrg.js b/test/parallel/test-webcrypto-export-import-cfrg.js +index ae203e1005de0ab4370bd611f4f2ae64bb7a9a6a..216ce5fd14001183e7deb2abadc93178e7a18a58 100644 +--- a/test/parallel/test-webcrypto-export-import-cfrg.js ++++ b/test/parallel/test-webcrypto-export-import-cfrg.js +@@ -411,7 +411,7 @@ async function testImportRaw({ name, publicUsages }) { + await Promise.all(tests); + })().then(common.mustCall()); + +-{ ++if (!process.features.openssl_is_boringssl) { + const rsaPublic = crypto.createPublicKey( + fixtures.readKey('rsa_public_2048.pem')); + const rsaPrivate = crypto.createPrivateKey( +@@ -432,4 +432,6 @@ async function testImportRaw({ name, publicUsages }) { + { name }, + true, privateUsages), { message: /Invalid key type/ }).then(common.mustCall()); + } ++} else { ++ common.printSkipMessage('Skipping RSA key import tests'); + } diff --git a/test/parallel/test-webcrypto-wrap-unwrap.js b/test/parallel/test-webcrypto-wrap-unwrap.js -index bd788ec4ed88289d35798b8af8c9490a68e081a2..1a5477ba928bce93320f8056db02e1a7b8ddcdf3 100644 +index bd788ec4ed88289d35798b8af8c9490a68e081a2..c6a6f33490595faabaefc9b58afdd813f0887258 100644 --- a/test/parallel/test-webcrypto-wrap-unwrap.js +++ b/test/parallel/test-webcrypto-wrap-unwrap.js -@@ -20,14 +20,15 @@ const kWrappingData = { - wrap: { label: new Uint8Array(8) }, - pair: true - }, -- 'AES-CTR': { -+ 'AES-CBC': { - generate: { length: 128 }, -- wrap: { counter: new Uint8Array(16), length: 64 }, -+ wrap: { iv: new Uint8Array(16) }, - pair: false - }, -- 'AES-CBC': { -+ /* -+ 'AES-CTR': { - generate: { length: 128 }, -- wrap: { iv: new Uint8Array(16) }, -+ wrap: { counter: new Uint8Array(16), length: 64 }, - pair: false - }, - 'AES-GCM': { -@@ -46,30 +47,9 @@ if (!process.features.openssl_is_boringssl) { - generate: { length: 128 }, - wrap: { }, - pair: false -- }; -- kWrappingData['ChaCha20-Poly1305'] = { -- wrap: { -- iv: new Uint8Array(12), -- additionalData: new Uint8Array(16), -- tagLength: 128 +@@ -179,13 +179,6 @@ async function generateKeysToWrap() { + usages: ['encrypt', 'decrypt'], + pair: false, + }, +- { +- algorithm: { +- name: 'ChaCha20-Poly1305' +- }, +- usages: ['encrypt', 'decrypt'], +- pair: false, - }, -- pair: false -- }; --} else { -- common.printSkipMessage('Skipping unsupported AES-KW test case'); --} -- --if (hasOpenSSL(3)) { -- kWrappingData['AES-OCB'] = { -- generate: { length: 128 }, -- wrap: { -- iv: new Uint8Array(15), -- additionalData: new Uint8Array(16), -- tagLength: 128 -- }, -- pair: false -- }; --} -+ } -+ */ -+}; + { + algorithm: { + name: 'HMAC', +@@ -210,6 +203,18 @@ async function generateKeysToWrap() { + common.printSkipMessage('Skipping unsupported AES-KW test case'); + } - function generateWrappingKeys() { - return Promise.all(Object.keys(kWrappingData).map(async (name) => { ++ if (!process.features.openssl_is_boringssl) { ++ parameters.push({ ++ algorithm: { ++ name: 'ChaCha20-Poly1305' ++ }, ++ usages: ['encrypt', 'decrypt'], ++ pair: false, ++ }); ++ } else { ++ common.printSkipMessage('Skipping unsupported ChaCha20-Poly1305 test case'); ++ } ++ + if (hasOpenSSL(3, 5)) { + for (const name of ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']) { + parameters.push({ diff --git a/test/parallel/test-x509-escaping.js b/test/parallel/test-x509-escaping.js index c8fc4abbb108a6d6849e8452d97d29187da2ebe6..825ba4c8dce775f401080a0522565bb7a087bcc3 100644 --- a/test/parallel/test-x509-escaping.js diff --git a/script/node-disabled-tests.json b/script/node-disabled-tests.json index 02f6e15590..9a92e7254d 100644 --- a/script/node-disabled-tests.json +++ b/script/node-disabled-tests.json @@ -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" From 441729c3a0b6f546382f52554e8891dbb4476a96 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:37:15 +0100 Subject: [PATCH 07/38] build(deps): bump actions/checkout from 6.0.1 to 6.0.2 (#49541) Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.1 to 6.0.2. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/8e8c483db84b4bee98b60c0593521ed34d9990e8...de0fac2e4500dabe0009e67214ff5f5447ce83dd) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 6.0.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/archaeologist-dig.yml | 2 +- .github/workflows/audit-branch-ci.yml | 2 +- .github/workflows/build-git-cache.yml | 6 +++--- .github/workflows/build.yml | 10 +++++----- .github/workflows/issue-opened.yml | 2 +- .github/workflows/linux-publish.yml | 2 +- .github/workflows/macos-disk-cleanup.yml | 2 +- .github/workflows/macos-publish.yml | 2 +- .github/workflows/pipeline-electron-docs-only.yml | 4 ++-- .github/workflows/pipeline-electron-lint.yml | 2 +- .github/workflows/pipeline-segment-electron-build.yml | 4 ++-- .../workflows/pipeline-segment-electron-clang-tidy.yml | 4 ++-- .../workflows/pipeline-segment-electron-gn-check.yml | 4 ++-- .github/workflows/pipeline-segment-electron-test.yml | 2 +- .github/workflows/pipeline-segment-node-nan-test.yml | 4 ++-- .github/workflows/scorecards.yml | 2 +- .github/workflows/windows-publish.yml | 2 +- 17 files changed, 28 insertions(+), 28 deletions(-) diff --git a/.github/workflows/archaeologist-dig.yml b/.github/workflows/archaeologist-dig.yml index ef56ddc1b3..61eaae8b7c 100644 --- a/.github/workflows/archaeologist-dig.yml +++ b/.github/workflows/archaeologist-dig.yml @@ -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 diff --git a/.github/workflows/audit-branch-ci.yml b/.github/workflows/audit-branch-ci.yml index 8e879e5113..c684dc4a8d 100644 --- a/.github/workflows/audit-branch-ci.yml +++ b/.github/workflows/audit-branch-ci.yml @@ -21,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: | . diff --git a/.github/workflows/build-git-cache.yml b/.github/workflows/build-git-cache.yml index 0f92280c6d..2110b5c95a 100644 --- a/.github/workflows/build-git-cache.yml +++ b/.github/workflows/build-git-cache.yml @@ -24,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 @@ -49,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 @@ -75,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 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b9d0e9e0b9..1cd14e0a31 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -59,7 +59,7 @@ jobs: docs-only: ${{ steps.set-output.outputs.docs-only }} has-patches: ${{ steps.filter.outputs.patches }} 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 @@ -127,7 +127,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 @@ -164,7 +164,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 @@ -196,7 +196,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 @@ -228,7 +228,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 diff --git a/.github/workflows/issue-opened.yml b/.github/workflows/issue-opened.yml index b531c29893..864dc02253 100644 --- a/.github/workflows/issue-opened.yml +++ b/.github/workflows/issue-opened.yml @@ -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: | . diff --git a/.github/workflows/linux-publish.yml b/.github/workflows/linux-publish.yml index a8434e1375..0c197a23de 100644 --- a/.github/workflows/linux-publish.yml +++ b/.github/workflows/linux-publish.yml @@ -36,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 diff --git a/.github/workflows/macos-disk-cleanup.yml b/.github/workflows/macos-disk-cleanup.yml index 217c446a48..b110170922 100644 --- a/.github/workflows/macos-disk-cleanup.yml +++ b/.github/workflows/macos-disk-cleanup.yml @@ -26,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 diff --git a/.github/workflows/macos-publish.yml b/.github/workflows/macos-publish.yml index 22181d98a3..6adb2a88a1 100644 --- a/.github/workflows/macos-publish.yml +++ b/.github/workflows/macos-publish.yml @@ -37,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 diff --git a/.github/workflows/pipeline-electron-docs-only.yml b/.github/workflows/pipeline-electron-docs-only.yml index 447fea5e28..b3e22f3168 100644 --- a/.github/workflows/pipeline-electron-docs-only.yml +++ b/.github/workflows/pipeline-electron-docs-only.yml @@ -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 diff --git a/.github/workflows/pipeline-electron-lint.yml b/.github/workflows/pipeline-electron-lint.yml index c83d61c65e..9bbb40b5cf 100644 --- a/.github/workflows/pipeline-electron-lint.yml +++ b/.github/workflows/pipeline-electron-lint.yml @@ -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 diff --git a/.github/workflows/pipeline-segment-electron-build.yml b/.github/workflows/pipeline-segment-electron-build.yml index 278834a448..a70ac7e9d5 100644 --- a/.github/workflows/pipeline-segment-electron-build.yml +++ b/.github/workflows/pipeline-segment-electron-build.yml @@ -100,7 +100,7 @@ jobs: run: | mkdir src - name: Checkout Electron - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: path: src/electron fetch-depth: 0 @@ -168,7 +168,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 diff --git a/.github/workflows/pipeline-segment-electron-clang-tidy.yml b/.github/workflows/pipeline-segment-electron-clang-tidy.yml index 90b42ecc37..7881e619d3 100644 --- a/.github/workflows/pipeline-segment-electron-clang-tidy.yml +++ b/.github/workflows/pipeline-segment-electron-clang-tidy.yml @@ -47,7 +47,7 @@ jobs: ARTIFACT_KEY: ${{ inputs.target-platform == 'macos' && 'darwin' || inputs.target-platform }}_${{ inputs.target-arch }} steps: - name: Checkout Electron - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: path: src/electron fetch-depth: 0 @@ -114,7 +114,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 diff --git a/.github/workflows/pipeline-segment-electron-gn-check.yml b/.github/workflows/pipeline-segment-electron-gn-check.yml index 93d37c9343..0c28e2c8c1 100644 --- a/.github/workflows/pipeline-segment-electron-gn-check.yml +++ b/.github/workflows/pipeline-segment-electron-gn-check.yml @@ -48,7 +48,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 +115,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 diff --git a/.github/workflows/pipeline-segment-electron-test.yml b/.github/workflows/pipeline-segment-electron-test.yml index 67141a7ef5..68c46f3ef4 100644 --- a/.github/workflows/pipeline-segment-electron-test.yml +++ b/.github/workflows/pipeline-segment-electron-test.yml @@ -119,7 +119,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 diff --git a/.github/workflows/pipeline-segment-node-nan-test.yml b/.github/workflows/pipeline-segment-node-nan-test.yml index 76a39a81b5..a97a9e638a 100644 --- a/.github/workflows/pipeline-segment-node-nan-test.yml +++ b/.github/workflows/pipeline-segment-node-nan-test.yml @@ -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 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 5143485c84..4cafc83c57 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -23,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 diff --git a/.github/workflows/windows-publish.yml b/.github/workflows/windows-publish.yml index b761ac86b4..991c535e15 100644 --- a/.github/workflows/windows-publish.yml +++ b/.github/workflows/windows-publish.yml @@ -41,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 From 8364b62f681574bd5d94207a0f433aa83b6a7a74 Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Tue, 27 Jan 2026 15:27:38 -0600 Subject: [PATCH 08/38] fix: potential dangling pointer in api::Screen (#49536) fixes a regression from #49506 --- shell/browser/api/electron_api_screen.cc | 53 ++++++++++++++++++++---- shell/browser/api/electron_api_screen.h | 26 ++++-------- 2 files changed, 52 insertions(+), 27 deletions(-) diff --git a/shell/browser/api/electron_api_screen.cc b/shell/browser/api/electron_api_screen.cc index 7652cb631d..d9ed7ea662 100644 --- a/shell/browser/api/electron_api_screen.cc +++ b/shell/browser/api/electron_api_screen.cc @@ -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 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( - 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 { diff --git a/shell/browser/api/electron_api_screen.h b/shell/browser/api/electron_api_screen.h index 19e910e05f..40c61aae7b 100644 --- a/shell/browser/api/electron_api_screen.h +++ b/shell/browser/api/electron_api_screen.h @@ -7,7 +7,6 @@ #include -#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& 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& 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 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, void OnDisplaysRemoved(const display::Displays& removed_displays) override; void OnDisplayMetricsChanged(const display::Display& display, uint32_t changed_metrics) override; - - private: - raw_ptr screen_; }; } // namespace electron::api From 3820d3ae6c8d0da9945a999c808b0d778250088e Mon Sep 17 00:00:00 2001 From: Kanishk Ranjan <68316017+KanishkRanjan@users.noreply.github.com> Date: Wed, 28 Jan 2026 11:17:02 +0530 Subject: [PATCH 09/38] test: fix flaky `BrowserWindow` test (#49364) test: fix flaky BrowserWindow test --- spec/api-browser-window-spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/api-browser-window-spec.ts b/spec/api-browser-window-spec.ts index f5ff750ef8..73d31312a0 100755 --- a/spec/api-browser-window-spec.ts +++ b/spec/api-browser-window-spec.ts @@ -6933,7 +6933,7 @@ describe('BrowserWindow module', () => { hasShadow: false }); - await backgroundWindow.loadURL('about:blank'); + await backgroundWindow.loadURL('data:text/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,'); 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,'); await once(w, 'ready-to-show'); const screenCapture = new ScreenCapture(display); From a7de47084bbd03e921df58a2e50ac01e72fb597a Mon Sep 17 00:00:00 2001 From: David Sanders Date: Wed, 28 Jan 2026 01:47:43 -0800 Subject: [PATCH 10/38] test: remove split dependency (#49551) --- spec/api-app-spec.ts | 8 ++++---- spec/package.json | 2 -- yarn.lock | 21 --------------------- 3 files changed, 4 insertions(+), 27 deletions(-) diff --git a/spec/api-app-spec.ts b/spec/api-app-spec.ts index 81ab51f10e..f074e0cd47 100644 --- a/spec/api-app-spec.ts +++ b/spec/api-app-spec.ts @@ -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)); diff --git a/spec/package.json b/spec/package.json index 545e704362..cc18041797 100644 --- a/spec/package.json +++ b/spec/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index 0b110f565e..29f8bc711f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" From e6be04a7a0774eae527f5aba981c4ab7eaeabc74 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Wed, 28 Jan 2026 12:07:14 +0100 Subject: [PATCH 11/38] fix: chrome://accessibility drift (#49547) https://chromium-review.googlesource.com/c/chromium/src/+/6870052 --- shell/browser/ui/webui/accessibility_ui.cc | 85 +++++++++++++++++++++- shell/browser/ui/webui/accessibility_ui.h | 13 ++++ 2 files changed, 96 insertions(+), 2 deletions(-) diff --git a/shell/browser/ui/webui/accessibility_ui.cc b/shell/browser/ui/webui/accessibility_ui.cc index 56cf4a0ffe..da4976e3f3 100644 --- a/shell/browser/ui/webui/accessibility_ui.cc +++ b/shell/browser/ui/webui/accessibility_ui.cc @@ -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::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::Value::Dict& 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); + } +} diff --git a/shell/browser/ui/webui/accessibility_ui.h b/shell/browser/ui/webui/accessibility_ui.h index 79dedf30c3..5634bfc22d 100644 --- a/shell/browser/ui/webui/accessibility_ui.h +++ b/shell/browser/ui/webui/accessibility_ui.h @@ -40,6 +40,19 @@ class ElectronAccessibilityUIMessageHandler std::string& allow_empty, std::string& deny); void RequestNativeUITree(const base::Value::List& 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_ From 82d350524e181241b2437e914a794c5ab13a48f0 Mon Sep 17 00:00:00 2001 From: Kanishk Ranjan <68316017+KanishkRanjan@users.noreply.github.com> Date: Thu, 29 Jan 2026 01:24:35 +0530 Subject: [PATCH 12/38] test: fix flaky `BrowserView` test (#49316) * test: fix flaky BrowserWindow test * fix: improved as per review --- spec/api-browser-view-spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/api-browser-view-spec.ts b/spec/api-browser-view-spec.ts index bb2c1e19a4..58ff7a087d 100644 --- a/spec/api-browser-view-spec.ts +++ b/spec/api-browser-view-spec.ts @@ -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,'); 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,'); view = new BrowserView(); view.setBounds(display.bounds); From 3d76d3a469d6fb22f0b6e137e210c81ae9e739fa Mon Sep 17 00:00:00 2001 From: David Sanders Date: Thu, 29 Jan 2026 07:06:47 -0800 Subject: [PATCH 13/38] build(dev-deps): bump @electron/lint-roller to 3.2.0 (#49565) --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index e3294ccfb5..49cd225135 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index 29f8bc711f..b1ab7a902c 100644 --- a/yarn.lock +++ b/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 From 0e161c18eb8e3ec227bf3a90c01ba2683bf86842 Mon Sep 17 00:00:00 2001 From: David Sanders Date: Thu, 29 Jan 2026 08:19:07 -0800 Subject: [PATCH 14/38] ci: rework reapply patches (#49552) --- .github/workflows/apply-patches.yml | 72 +++++++++++++++++++++++ .github/workflows/build.yml | 39 ------------ .github/workflows/rerun-apply-patches.yml | 22 +++---- 3 files changed, 84 insertions(+), 49 deletions(-) create mode 100644 .github/workflows/apply-patches.yml diff --git a/.github/workflows/apply-patches.yml b/.github/workflows/apply-patches.yml new file mode 100644 index 0000000000..980a3b3cba --- /dev/null +++ b/.github/workflows/apply-patches.yml @@ -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 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1cd14e0a31..f940f98067 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -57,7 +57,6 @@ jobs: src: ${{ steps.filter.outputs.src }} build-image-sha: ${{ steps.set-output.outputs.build-image-sha }} docs-only: ${{ steps.set-output.outputs.docs-only }} - has-patches: ${{ steps.filter.outputs.patches }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: @@ -74,9 +73,6 @@ jobs: - CODE_OF_CONDUCT.md src: - '!docs/**' - patches: - - DEPS - - 'patches/**' - name: Set Outputs for Build Image SHA & Docs Only id: set-output run: | @@ -109,41 +105,6 @@ jobs: container: '{"image":"ghcr.io/electron/build:${{ needs.checkout-linux.outputs.build-image-sha }}","options":"--user root","volumes":["/mnt/cross-instance-cache:/mnt/cross-instance-cache"]}' secrets: inherit - # Apply Patches Job - apply-patches: - needs: setup - if: ${{ needs.setup.outputs.has-patches == 'true' }} - runs-on: electron-arc-centralus-linux-amd64-32core - permissions: - contents: read - container: - image: ghcr.io/electron/build:${{ needs.setup.outputs.build-image-sha }} - options: --user root - volumes: - - /mnt/cross-instance-cache:/mnt/cross-instance-cache - - /var/run/sas:/var/run/sas - env: - CHROMIUM_GIT_COOKIE: ${{ secrets.CHROMIUM_GIT_COOKIE }} - GCLIENT_EXTRA_ARGS: '--custom-var=checkout_arm=True --custom-var=checkout_arm64=True' - steps: - - name: Checkout Electron - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - with: - path: src/electron - fetch-depth: 0 - ref: ${{ github.event.pull_request.head.sha }} - - name: Rebase onto Base Branch - working-directory: src/electron - run: | - git config user.email "electron@github.com" - git config user.name "Electron Bot" - git fetch origin ${{ github.event.pull_request.base.ref }} - git rebase origin/${{ github.event.pull_request.base.ref }} - - name: Checkout & Sync & Save - uses: ./src/electron/.github/actions/checkout - with: - target-platform: linux - # Checkout Jobs checkout-macos: needs: setup diff --git a/.github/workflows/rerun-apply-patches.yml b/.github/workflows/rerun-apply-patches.yml index cdf689ce70..61423b2bbc 100644 --- a/.github/workflows/rerun-apply-patches.yml +++ b/.github/workflows/rerun-apply-patches.yml @@ -15,8 +15,9 @@ jobs: rerun-apply-patches: runs-on: ubuntu-latest permissions: - contents: read actions: write + checks: read + contents: read pull-requests: read steps: - name: Find PRs and Rerun Apply Patches @@ -33,11 +34,11 @@ jobs: PR_NUMBER=$(echo "$pr" | jq -r '.number') echo "Processing PR #${PR_NUMBER}" - # Find the apply-patches job check for this PR - CHECK=$(gh pr checks "$PR_NUMBER" --json link,name,state,workflow --jq '[.[] | select(.workflow == "Build" and .name == "apply-patches")] | first') + # Find the Apply Patches workflow check for this PR + CHECK=$(gh pr checks "$PR_NUMBER" --json link,name,state,workflow --jq '[.[] | select(.workflow == "Apply Patches" and .name == "apply-patches")] | first') if [ -z "$CHECK" ] || [ "$CHECK" = "null" ]; then - echo " No apply-patches job found for PR #${PR_NUMBER}" + echo " No Apply Patches workflow found for PR #${PR_NUMBER}" continue fi @@ -57,13 +58,14 @@ jobs: continue fi - # Get the job database ID for the apply-patches job - JOB_ID=$(gh run view "$RUN_ID" --json jobs --jq '.jobs[] | select(.name == "apply-patches") | .databaseId') + # Check if the workflow is currently in progress + RUN_STATUS=$(gh run view "$RUN_ID" --json status --jq '.status') - if [ -z "$JOB_ID" ]; then - echo " Could not find apply-patches job ID for run: ${RUN_ID}" - continue + if [ "$RUN_STATUS" = "in_progress" ] || [ "$RUN_STATUS" = "queued" ] || [ "$RUN_STATUS" = "waiting" ]; then + echo " Workflow run ${RUN_ID} is ${RUN_STATUS}, cancelling..." + gh run cancel "$RUN_ID" --force + gh run watch "$RUN_ID" fi - gh run rerun "$RUN_ID" --job "$JOB_ID" + gh run rerun "$RUN_ID" done From 92a3f7d6c104dc98956bf2a12e226e358a9ee0ee Mon Sep 17 00:00:00 2001 From: Michaela Laurencin <35157522+mlaurencin@users.noreply.github.com> Date: Thu, 29 Jan 2026 13:12:42 -0500 Subject: [PATCH 15/38] ci: add workflow for commenting with new 'ai-pr' label (#49564) * add workflow for commenting with new 'ai-pr' label * Update .github/workflows/pull-request-labeled.yml Co-authored-by: David Sanders * add pr autoclose * replace with specific secret Co-authored-by: David Sanders * specify repo Co-authored-by: David Sanders * update wording of comment * chore: use GH app token * chore: fix indentation --------- Co-authored-by: David Sanders --- .github/workflows/pull-request-labeled.yml | 31 ++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/.github/workflows/pull-request-labeled.yml b/.github/workflows/pull-request-labeled.yml index 1662057239..0c540cf165 100644 --- a/.github/workflows/pull-request-labeled.yml +++ b/.github/workflows/pull-request-labeled.yml @@ -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 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" From d74fcfcecb3f99c98def1b8b6bbb23b6bd59ea68 Mon Sep 17 00:00:00 2001 From: Jan Hannemann Date: Thu, 29 Jan 2026 13:38:26 -0800 Subject: [PATCH 16/38] feat: msix auto-updater (#49230) * feat: native auto updater for MSIX on Windows * doc: added MSIX debug documentation * fix: allow downgrade with json release file and emit update-available * test: msix auot-update tests * doc: API documentation * test: add package version validation * fix: docs typo * fix: don't allow auto-updating when using appinstaller manifest * fix: getPackageInfo interface implementation * fix: review feedback, add comment * fix: missed filename commit * fix: install test cert on demand * fix: time stamp mismatch in tests * fix: feedback - rename to MSIXPackageInfo * fix: update and reference windowsStore property * fix: remove getPackagInfo from public API * fix: type error bcause of removed API --- docs/api/auto-updater.md | 31 +- docs/api/environment-variables.md | 16 + docs/api/process.md | 4 +- filenames.auto.gni | 2 + filenames.gni | 2 + lib/browser/api/auto-updater.ts | 7 +- .../api/auto-updater/auto-updater-msix.ts | 449 +++++++++++++++ .../api/auto-updater/auto-updater-win.ts | 5 + .../api/auto-updater/msix-update-win.ts | 4 + .../browser/api/electron_api_msix_updater.cc | 527 ++++++++++++++++++ shell/browser/api/electron_api_msix_updater.h | 14 + shell/common/node_bindings.cc | 1 + spec/api-autoupdater-msix-spec.ts | 336 +++++++++++ .../msix/ElectronDevAppxManifest.xml | 50 ++ .../api/autoupdater/msix/HelloMSIX_V1.msix | Bin 0 -> 68114 bytes .../api/autoupdater/msix/HelloMSIX_V2.msix | Bin 0 -> 68114 bytes .../api/autoupdater/msix/MSIXDevCert.cer | Bin 0 -> 774 bytes .../autoupdater/msix/install_test_cert.ps1 | 22 + spec/fixtures/api/autoupdater/msix/main.js | 96 ++++ spec/lib/msix-helpers.ts | 149 +++++ 20 files changed, 1707 insertions(+), 8 deletions(-) create mode 100644 lib/browser/api/auto-updater/auto-updater-msix.ts create mode 100644 lib/browser/api/auto-updater/msix-update-win.ts create mode 100644 shell/browser/api/electron_api_msix_updater.cc create mode 100644 shell/browser/api/electron_api_msix_updater.h create mode 100644 spec/api-autoupdater-msix-spec.ts create mode 100644 spec/fixtures/api/autoupdater/msix/ElectronDevAppxManifest.xml create mode 100644 spec/fixtures/api/autoupdater/msix/HelloMSIX_V1.msix create mode 100644 spec/fixtures/api/autoupdater/msix/HelloMSIX_V2.msix create mode 100644 spec/fixtures/api/autoupdater/msix/MSIXDevCert.cer create mode 100644 spec/fixtures/api/autoupdater/msix/install_test_cert.ps1 create mode 100644 spec/fixtures/api/autoupdater/msix/main.js create mode 100644 spec/lib/msix-helpers.ts diff --git a/docs/api/auto-updater.md b/docs/api/auto-updater.md index 350bcb099b..5472904f10 100644 --- a/docs/api/auto-updater.md +++ b/docs/api/auto-updater.md @@ -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\ (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 diff --git a/docs/api/environment-variables.md b/docs/api/environment-variables.md index 5b6305edac..b2acc3ee3f 100644 --- a/docs/api/environment-variables.md +++ b/docs/api/environment-variables.md @@ -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 diff --git a/docs/api/process.md b/docs/api/process.md index 78685084d2..4ecdf8a464 100644 --- a/docs/api/process.md +++ b/docs/api/process.md @@ -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_ diff --git a/filenames.auto.gni b/filenames.auto.gni index 95c779b8e3..87434e4539 100644 --- a/filenames.auto.gni +++ b/filenames.auto.gni @@ -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", diff --git a/filenames.gni b/filenames.gni index 21b57205fe..d8ca1fec8d 100644 --- a/filenames.gni +++ b/filenames.gni @@ -279,6 +279,8 @@ filenames = { "shell/browser/api/electron_api_in_app_purchase.h", "shell/browser/api/electron_api_menu.cc", "shell/browser/api/electron_api_menu.h", + "shell/browser/api/electron_api_msix_updater.cc", + "shell/browser/api/electron_api_msix_updater.h", "shell/browser/api/electron_api_native_theme.cc", "shell/browser/api/electron_api_native_theme.h", "shell/browser/api/electron_api_net_log.cc", diff --git a/lib/browser/api/auto-updater.ts b/lib/browser/api/auto-updater.ts index d433cda0e6..d0c61dbc7d 100644 --- a/lib/browser/api/auto-updater.ts +++ b/lib/browser/api/auto-updater.ts @@ -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'); } diff --git a/lib/browser/api/auto-updater/auto-updater-msix.ts b/lib/browser/api/auto-updater/auto-updater-msix.ts new file mode 100644 index 0000000000..5b8ff6856c --- /dev/null +++ b/lib/browser/api/auto-updater/auto-updater-msix.ts @@ -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 | null = null; + allowAnyVersion: boolean = false; + + // Private: Validate that the URL points to an MSIX file (following redirects) + private async validateMsixUrl (url: string): Promise { + 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 { + 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 = { + ...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 { + 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; allowAnyVersion?: boolean } | string) { + let updateURL: string; + let headers: Record | 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(); diff --git a/lib/browser/api/auto-updater/auto-updater-win.ts b/lib/browser/api/auto-updater/auto-updater-win.ts index 255049cec3..851ad631ba 100644 --- a/lib/browser/api/auto-updater/auto-updater-win.ts +++ b/lib/browser/api/auto-updater/auto-updater-win.ts @@ -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') { diff --git a/lib/browser/api/auto-updater/msix-update-win.ts b/lib/browser/api/auto-updater/msix-update-win.ts new file mode 100644 index 0000000000..c6500b254e --- /dev/null +++ b/lib/browser/api/auto-updater/msix-update-win.ts @@ -0,0 +1,4 @@ +const { updateMsix, registerPackage, registerRestartOnUpdate, getPackageInfo } = + process._linkedBinding('electron_browser_msix_updater'); + +export { updateMsix, registerPackage, registerRestartOnUpdate, getPackageInfo }; diff --git a/shell/browser/api/electron_api_msix_updater.cc b/shell/browser/api/electron_api_msix_updater.cc new file mode 100644 index 0000000000..605e7a45d0 --- /dev/null +++ b/shell/browser/api/electron_api_msix_updater.cc @@ -0,0 +1,527 @@ +// 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 +#include +#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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 reply_runner, + gin_helper::Promise 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(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 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 reply_runner, + gin_helper::Promise 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 emptyDependencies{nullptr}; + IIterable 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(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 promise, std::string error) { + if (error.empty()) { + promise.Resolve(); + } else { + promise.RejectWithErrorMessage(error); + } + }, + std::move(promise), error)); +} +#endif + +// Update MSIX package +v8::Local UpdateMsix(const std::string& package_uri, + gin_helper::Dictionary options) { + v8::Isolate* isolate = electron::JavascriptEnvironment::GetIsolate(); + gin_helper::Promise promise(isolate); + v8::Local 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 RegisterPackage(const std::string& family_name, + gin_helper::Dictionary options) { + v8::Isolate* isolate = electron::JavascriptEnvironment::GetIsolate(); + gin_helper::Promise promise(isolate); + v8::Local 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 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 exports, + v8::Local unused, + v8::Local 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) diff --git a/shell/browser/api/electron_api_msix_updater.h b/shell/browser/api/electron_api_msix_updater.h new file mode 100644 index 0000000000..526392576b --- /dev/null +++ b/shell/browser/api/electron_api_msix_updater.h @@ -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_ diff --git a/shell/common/node_bindings.cc b/shell/common/node_bindings.cc index 3b887fa6ab..f5d6a22e31 100644 --- a/shell/common/node_bindings.cc +++ b/shell/common/node_bindings.cc @@ -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) \ diff --git a/spec/api-autoupdater-msix-spec.ts b/spec/api-autoupdater-msix-spec.ts new file mode 100644 index 0000000000..f66232d305 --- /dev/null +++ b/spec/api-autoupdater-msix-spec.ts @@ -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(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'); + }); + }); +}); diff --git a/spec/fixtures/api/autoupdater/msix/ElectronDevAppxManifest.xml b/spec/fixtures/api/autoupdater/msix/ElectronDevAppxManifest.xml new file mode 100644 index 0000000000..742fd39fa9 --- /dev/null +++ b/spec/fixtures/api/autoupdater/msix/ElectronDevAppxManifest.xml @@ -0,0 +1,50 @@ + + + + + Electron Dev MSIX + Electron + assets\icon.png + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spec/fixtures/api/autoupdater/msix/HelloMSIX_V1.msix b/spec/fixtures/api/autoupdater/msix/HelloMSIX_V1.msix new file mode 100644 index 0000000000000000000000000000000000000000..71d5619596551603abf31b646e90a2cd36f4d913 GIT binary patch literal 68114 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

awIS%FnK2x5@{x7JZNCWV6sjkfuIZoS4cRK zNQ1H!Dz5CNL4$-r#iU>d)YtZ-Vf&MmaZ7Hp#%` zz;b|_)i+u0(~a~z zACE5qDSO=`1Z^j+iMT3Ml%t(tL(U#wKGVD(of>t^IEE{POfZeWECKf{L?{i8Im#K< z1L)=hy4K7*?4=5T$Z>{Y7&^L*v``n&jR6o7;BEn+nqMcF5DK?n2Kq&#PjdX^)m;glvBBum`H4^Ov>x~A%S_Y~LfvP#c z7nh=ew1yDg0MX#Y!;YW=R7k<#YPbvph!-h05L|$891?hd5CjA^NZOm0@M%`pi{a5 zPcuL*H45;Q;9)nu1C-qaf#CuWT?nEEAh)>72~Gjd`ntdYPYvXxD6lU896*`!u)E)a zAYg!HDJ9Yj>jC@3z+}F<9T5k>b3s-X7)EgBz{*JtfPW6~7@&*~ID-P7l|rpREMcSc zBQT&h3AmDVERklAAH>oYSg+(SZ}f-Ih5)^xfka;e2C2Zf&7w=B`Njk|GJsZVyydEv zK&>cbqfP_arVW@hgZ}ExY$Lq_jax2YqlK%yaTn-9-va(l(~nr=A^HGP7UhiK1m;-; zL0J7Xkg1Rr07nQV(zFN`6TqyX5L@uD#{w5&`w*A1XlDcmfcU8b;VTemAwd%e@=?yc znE-FW50oisAm;!Xm@A|}jEB7v(&h{L2*H_1GYY)NT>}A21p|B?bW4L6aHdnwE*x^% zS{87g6)0D|0z7kv`e>DDBKq_ksBMSg2ONzv07oar5i|smh6Xa|Zb?Y5J*T^099Qe5Dkhv>_w&sSmvKZ+9fE+V1A7?ld=xAfus$$# zaBLdL4aoi>2oC}q0PtLpm3$q$X=v)71j7n;iD~3}0#Nn%kGVN;0H2%?yop6LKs7!&r6#wTqyV4s$*EJ{t^f)%l$>Dpg5}9z zC4n74W*X4dPXKL;luCA0QMUgk?HEo(d6EtQWH%@Ct_nS9Y0$E1)Z2OYCsG z5Dp0(>_0J}%V8Vy$MD2dL6^gJ)?=K1Y(W!<8XDSix!QI+n}Se(pn-P{9?CrCE>KtL zK$3hK-dB7LbvK0@uOEsVsQmxJgbHQA0A^U%^J6!#4F1OX%?1$F#;ckv(O~yACSUprhzoCyCi<8XRF@{N!hDk zC4%)sD@*0qqStj}-@8I;7tf~zHE$7slm#4K*a4;0vqh(4@An(Q@(eRhhRh7H4As5= z>ISkE>^Kybg(KX>E*gNQF2q$qE5?aE5m5WdZ+cYAjtC|MmTQ=h)6COq1^}akU>#{c zVQLUe(*7q5+E{V9wj4CXd!L$ipaZ|j>V8c>gyaDs{TbIOym~p1fnajQSc{!K?2mUV z716*?GN_kYE90FeUM%jU>^=W*@enXg3-$NLXNIBRfRW6DvI>TP$}{UsOAi-N8o;PQ zl85;f`dW-d0R==5)g^ZR0}mUa06vLhIiP@oh=10K!~1JZ4|TLtPHR;hh!g^HWAdTt z0lhiMdq2181E&-qmG`>X@JdjE5N!9rv^ReEzh*qRf&o`b%bgD{NkhZApyMZq7rK4bsCAym@i-b6cLpwV>#N=Up+LoIv&69_5L z;xCRf0NTE@13RpVPmyH6=%fy~(!yKZr3tRwhEggo!_X{*(StJ5sVFBeP(tIdU>G%! zH&uaJ17-~kI#6%kz7BYbY6HAK;}9q*Ko$UgqQs}nT!Cw))WFJ|s)czt)CwAzW=una zJV5b0(1p^Q8odK_amj#I4w@F`>7dmEXcEGsoNAx|;u9KJJ3#J-;qVDnz$4BHf-nO} zzNW=A2cUma9Vos;KQs&qP1tG3|F=yt@gNvDycL}(wu_(x8u~%ZvkrO0kpoETZ4lX8 zT*artfJuGG{p=GY)|S3knJ- zV%kwoYg~Y`IuxSf=Lt~09EeiPI1v;tF zz=D?-@OOYJ_RG;ub^-w11@xAJSVFrY6cf}++XBN6R2@`p3`6Kdpz17Cvnn*nm_QJG zp-1XL1EQbB2;u_D?MY>zR|u+q$D3sQpkWmQCN!702aPM_i7g0tn~p_zD+D2jAPhs$ z&ICpdV#Easi+HFNJ#>M7XcBB0U~&dfDcd9y1zmtj*;G(Ee*_#Gp!L4fz#<&vI@lxx zndL85RR)Ah(}C}x{Kx`3SJ*IAac@OA^~MACN?`UiSTxBvfUPHN2@taeJpuLx1Ta|; zI|P{r%{FAF0GMF~X22EzhC2@lW4`yAWLTlKs|Ktm4Nu=USM_9qGf+r&p{f?cIAk3N zY_ISGB4FM$g1R0CW*4q8$_c9E4|Wc6O){w|073=4wWgk8>jXS-n!q%)AGiXX!f!*i z8Hb><9L53)Ih4v2+YEm>K?9Tr=F~01cY$7X1hDue+NqQlY6S?PK-~k1Y$DtYXyu@7 z5q>$^8I}MoTP{&fVCM3k*7UXwpW5Az%!4HcDV< zp%_7UZ-LDSsDxn&&`l3)v=dbP!t|lu^#piPU>(2DJ2J&a4{>||tb;8F?liEH3e_Z_ zP%^#)6hnK)NhJ@Oe6X6}AS>?iPf7d15NhB95II8&umK%8*nl1d^;8isiGX$&U}u7} z1j<+;S9YSDP;S6Jl!9c}~Oum<&90yF~RKr9Qvexe-A zQ~WmnT(lF)2dI(;OKT~Q2MzivXa!Xhg<2Lw&}{=0mXIAL!Z3t82ONX44s`Fp8tmxt z`=(Mt51Pp|0AU6YI}q`jeyQ?Wk6@VOH#%BeXOj%>b9LJWr%}(>f~&sEfv@HX+S+7? zVQmZPMpJAJ%F1=^8{*xxC@o;abZ@hMVw$m>R zVh;LOJZQZsA}-S8)Fi29x?mb8tKR`^=ZIC&l0l$lzO~|L1N4g02F8 z{%3DJ{cfo~QeUk~VtUzJIQhSMLvuZg90xOs>JItzpSI{x{i=A)q1 zuSd=JpBqTXApF4JKmWl2fhZ`BMaiN2=~sf{FN4GHr_8^fGXHx|nYCo4q``MGWxndlyjo^l#s>Ox6A>b7cNP+daWt=2=tP0{1oUUVo@E!+kDnKjHP@)!tv`gBmoJW`a^^5 zfX|P_*1mstkm0W#9T`buy3Hq8sJ_bmHEE+LwhR%N*|&e8G} zd$easVE)0i)ShWMvca)Y`0%EOa~6LFb%C2+^YIVD9|q)Hl`bb6Mh>(-H1eER)eAdE zjqXjdW+q{&d@zNsYUrYaKN2#5drzzD9gi9qw|=Lt7t^#5+}++YurB9%pI_7F@=m$$ z>(HtIi8(GYB^orbKF1*n zAQ_cnYm>t{Rjr>w-i%fqX|34HB`SL^KUs#REzlJd6@){r%QuNdfFGrJIGTXUZ18j` zAxkB(2=o~`NrG?2SSVV^kGpM6BI=+@WpdEMn&M{z^nqC%KQP)5#!_ibf>%j+JBuIE z(i@hH!)pm+Dg3DbZvo)(W=Y^^3WzqqH&Pxb;D;yyJPDr09~BVe2>?7~1SnvH7^0s^ zfqwuCVBw%a1LHq7Akc%900X)WVJ7FmHBsOO-T)x@uopN=46(rDlhwc_mP#JH*&^~v zYrsZ?5Kl4&_1Bkf<3SJc7-2jFkN8hdKh!ZdC)^7&QG})LExT9-ztnAbJN3q_c6Wyn zu)G~V_%YNAmu%Vf!)D&d;a&I26B~Z(ztpdwVCKtY@tGE&nnW zp?lG5hpx?cRa0-H%u*CtIL;ce@6 zfx;|6lTyTi0v+>`(v0wSc4^pc+(V)N;^`e3JhT*#WhumiNeb}j#^e8`OP<#nk4rib zfYE&$#}7FacFwUdi$t`M(u~8Wff3>XMqDr8N&?>@ItT(7MrP0@61X)51yUP5Rz!wINN!dOa)MZmN`gV7A)ho<<2hjbq(cx?v!hY-YYQw^v=7&G2N zqb_6|SO9YhOdMbWsKHx+XdD0l4PXH@f-ErNX+R194Y)Og2Q=^s{)R6-P(bku->CZ& zUx4_j;3vN5^iKtR5CO(Y-B4h^08<&+?MhWhTonI?P2u@5U{XQbX>JDDpxb*KH`v>_ zYs2rrtm;IbsR-JrInXq&MkZ&Sqb}n1?gnisvt@|851WLUoT`j0efCoKcy9`B$gcjf!melT{0jl@4vxKY8T; zGAw!K0khZ3=Nzphg%6(I9DETYD2moe6D+#^Od#_+L2zl(tTu>KQq}R!!tgonErVi<$h-5m&n1DB z^|q*btms_AOazqj4zeJD-a!@w(m&?0zc)B7zDI8j%aKv9y?lMt57g5CP|A~Dunf9F z1)T-8_Rr&=m+}Ij)t{E~|H)TJe;4)OSIWPCMg0HEuZVva_5bs~BK}>}|1Ro(7xkdv z|6SDoF6w_5^}maH-LU^Rih90(F6#gOI-vigr2qY5px-6^?-v8Pi$O0Z`rmk;{`ZT4 ze!m##|DhKH(fvJmvTem`6O{cQQ)d$ug8rY<2V2_7W> zdYPT9rktv}n!Kcpsun^^6a4!3;MH>f>e=#7tlVE>e?MFP53j4}7WX*Ch57?1*C)F+S5Jvb%aSD5X(%6`DWn&CN+n?@Q=wR)NO9~t z_nPrH{t^3&Pp_ zy(H!iyjVY;YOT>GeUIu@82J}%H3?_(_?C{tuVN+_=pV8A5ML}%WZ?~)TpIcDDvFzr zh0}udR78RL%*$b>iL{=S)8xV@4_8Rxsy9wYnHD+es4k|W?f$}UiSf)o+ z!?nk)_~;fhT7BwZ)_tfh`@K!b%1 z3x?Z=bLXg+Z1TNYy7n8_>7qqc_O0|xgQyU&tEvk>h(-pZ!Rq@DPa`CwK6(`Ve22xa z3sy3 zGQ8~T=xC9&Q;7|kFYK2JUB*aZxTnQv0vHbbW&;I&96=7g*-H$*$s0z47rDT1kba7= zMp76g(nCZ*DgAQ^>SCZZd&u5jy!@Z4-q5L@O zaMdak*bnIn!ly?$qX~bx@{83!X{3)yr{#*jfxRc=YQS~-1=jNRNChRmGpg_C?X7dO z=t_pBa7pndHtHVjZ2H&Po#`#_t~W(hZDNbfNXU_>O7216OEygt+uwz* zWy4|Xrw(3kwFc2U&OuzmsLE($6B~5o9{i&X10aKVWYd%h{5+D9Bk5hDWXgaw=!hx} zbzlNVdS{%2dSUd>BS9FAM!Ex&lPwJ~JJ%?^6NrfhyDbg9GbH@stC{ZHZ7Nr)zNk?HR=e+iHe5chQSuOpzxH40?>5sDb>Zvt?Hn4Mm3^nJY2 zD0Bt4_GZJO3Ah4!4!8g)!+r|YiJE`YaG)NqPK)IiGbb>3f6-(&{pmH_{onXN9FMn* zYdA0s=;28dYU2r@CJ!7WKZ>^u3(dg?lIy3XKZ0?mQq2kIrAG%Ds$D8&3O+4ghq|IE zu9_XY(G105N^7hL9#W4cQdp zMpZ2-J6UN$pQl>PHcyW$8q}wJjzMxgc*YFlywYjcghAM2u5N1S77jJ%LBIm z?gxz!BgknKpazX7d<0O1aY_fiL%PA({3#$452!&z!Px#Oj5b9<(L10C={XR79ih45 zbZ`-P1bh+k5`mSO%&Mm;mznVYt>?`PBIZ3_lG+*+4%|QrLIF4k2iG9|PXQklFlX_d zQWgot3J^Tc)$Qml36(u*I^$Y=?;1S5lVFah4Pj;I0SqQT}tj0;>L&C_&yt3ArlZeqiiL zV2d$K;1{ox_AXzc^hCncb2ZI4c8g`^X{f(oLDV_8afMu{W|{I-H`u>|D}New6bw` z7k9uqJG;4fIr@oV{jiHcRMuyk2UfdWq|vjEoo~FV7F#7jYvN-kNAB7r zoUUCec&%2c+Q73$sXD*B!);?*NiVRN%`2^-dN3#T@QbcGRME4?gU*>!Jr>i#zDBwR z2G!&oB@Yd{Yd_!7)RWn)+3zS=<_a7cpoutaSsU-(KJbJn*ljua{Dje!5(|IWidvhm z;D=1{+M9FYVjsU2M4fo@xR7ycBjsIM4|?md-R`<~L9G*=9UGZu+LL^;{b8ILHw{f}D{-34h1LZf#Ict?>!+09UPh35PE~xY2dd zzR)|VWc=Y%)}2-jQSvPTf;t&wCiA}0)ChXieZ`;et?-xoRcn@0PcA{a{b=ClT z`rva)6NRU0x)$XcmZ_6UTIvfI7w(jwG1eYYuv|VI-k+Xv!K5srOiv>7+{~B?xk~25 zc~YANsX4`J%_$}f>V$6|;y3ovKk!GqKW5nCU!v~T96cEnbN@l& z;sPkG)siu~@l;cr7*O(nLJr zIN8<2sF=91>eaByNpz1QM?_+wF);njYwh3*soMzuxH%VYH(Ea`Ly~1T*BRL(O1x!~ zp}x$KV(O%4y2+LaIvF)K(&i`ABtBfRb3a;MKP$R&+K_BnNuM>^pqBJZ1!A2*bff+3 zidiCZfzVmJ+(=F01WMDnIpV9J^$F@dYGpwg0h%g_V>LT1ydtsjtr4p|q;7ruleIUf z#o9=h&79RFJg+2wyRVR9ozI}xi4qG@?Uqd(L{ze==V63!TivN)yNKf$Z?@bGn`5(Y zgv7LDt!v{_OW2sd(k@(5woi+1W{D?Q{wQyDU4ScPtSsk5tEiRDtOE7=49=J5+YW!E zr81ooS@m>{-AZuSbAMu~l5W(S(x$E9QxysGj&lQ;g^zN@;}^5;Nxbeui+*J^sMCCw z&)B(X!tDo7HNJsOm+&Tv>_)lq+NXuz?-E*Ki&sW~psuoR3mKr3<(jx@J-%Bu zzL_dbJqGvD)6z@iH;7Cv^9vpSE+O`8X7JK=Q&)qkW8!7jqblm$RY%(4U0)_;Ph3Ay zY>~{M6e|*dMNfnwEvDAHk_YT9h+d~XrIB=Tc2f`%o+pZa-r%?wed$JF^%9A|{k4?) zYTZUDePU1cZ4VRc6_0Pc8)F~qaE*Rae#q=fvqx`-R6Xrg)LqgCPgsNN8H`ijmD8X1 zu_=EyrID?#L5`My$@#`<$eB-ya>>GwywxtYi_EhQGNPLIissK)V^S-WZ=G1+D({r- zFc@MtxlD=k=k zBIGP#w99ZQ=ks!Yl^l@?{bRS>7;5JpSa$Y|*4&yZVx@an|1>jyBz+{#;z#@41QFkl z=1h+YRzI?JKX)bKmzO=ybnRuK@ph5YrrNVA1pL+scFclm)M98|vBfL2`+I_;rWcGk zb?&WtmhS0gRqpd_2Ao?qUcFKj?=*CMblvs2lF17fg||yrLwYOIRY%TWKk6(#q8+x}$RLVodSJXw#j4zOzj{i^eT8yB(eCjD zyDKE+YwyWy*Wc^p%`Bdxvn#Ytbgj|k*vKW z!q)Ni-KA@(D=ZW@>+bUfIVT;{kOg%-=0)mRg)9yv)Vs( zwxzH~XZ*sOtF4PUC)qXQCx(m)mIFzHBpEIy9_!f1@E?9JzvE@|-tlE&=;+O?jmf9t zl@#jX3<8}lnQtQ6-|pcy<|b&&ci>B3K5U7!8cya&-Y5v3=us+`Y$O)jS2|Sgay-u! zxmHBiBWSY~OjCQ<&~j6bZ%9LMqFnqag&2;*csc(T&n+vWhtHI=dK{Nu@KjrTJF#!q zmkV2i54UTgTwKo!$eFEH*i{X;6@5w%NMvJeUbX9Uf7#nkzc7{^#z4#77w}<=Re!gz zUSfTDk092OSS`(f%T{Rh#5XtI`out$t@BAL*8g(z*kYWwjiZ|+Cy zb{G?V(pg#8CX)Ms8E&^;pU#yjmYZf5<&@JBt1#+& zO>rzvsyrbp)o#Ehv`+kvJ@xxN;nZ{&&ok#o*KVFy5-eyiHaTpX^`Zc7Lf;WM@Z^i` z=Nb~Oi!8d%T@>;t4X@DWdL}BKsA*Ns>oY9PJOyOAhA$dF;IjIPIT}3tPMLPh)U+_4 zdDhD-e!Rj+>;5X+#Tl&!-L)s-cbuu~HYrZNGwtn9Ln=3zc*~u&<_#BA%p$Nieb>_L zwqJ3&Ld&FR+c`^rsJ$*~@e6M2I^IT;{@(@yn8OZ{U z0+D2<=h!NuaLcL&KkFwJ)fY}wg{O$}YSGTzPf55HhZV6|dHBeLH}`A@1(G)x{fgP1 zLerEgcJhZPpwbZ!E;+a#OK7Z+(MA{eCEa*>YI@0?pMhtCy)%Ch7ug7aG;FP8ATf zrza(>Q0JDYZJD2ug;AaeedxMNp6M3TR>86wNUjvN>E%yp7c4CFjbsz^%@JKl>Tt;y zarjkuR(dsnS~4G&UcJ@&M!}*V zE4Sg{57*k6`0N<;P31NB#GYz#404#S^!*vfk4gJx%2+?|BX+EGvCpa93O~8UZBwHQ z>_#U`V`+jT4a`Tbn=4B!RD8^}$nBh&W?=8StP+~{sIo4_QK8R-qw}@>F_&kFv(yi* z;y=Oo!*~rkET;SlTR`O9NBbX49>E^!nslDPpr%1=eiNpW7!JY>>7*8Yo1w6VPcK z^Wet5yR{mhvTU2XR)Z_O8FSz^QSrR!i!I5i0E0u#r1ZVp^XODc)(>?f^O|ZfwnNbSG_q**a*HUEsM&5W`TR;7Em!x{j zZ?e^wej{_*e_Q?;_94jxZGqG$0&c5!uTz2ydfi0$J4b4y+-quOj7s(?su!qCRCD%k z!)|U8jceu~4mGVAc#D&&#b#i|XT%*As@jTD)PK0%GB?&jv70sBJqP5_GW>H(vXl$eSP{HloI>5nLm|Ah| z>z5$0LR&G*=2s*1_tZaVW?|X*eNpv_8b0Rib!d|xdqPfO*^-jO zk4{T%+ACjm>>U63y)5&-+>-Un(;v%=vhq3W{e^CoynmnihPO;@eK3A$LZ@?Z?efXc zN0kG)ON*D^-d0>MXII7eMcL%+)t~GPZOWlPG>{bPWH05aRk0Enj7#O4wjK(XMkkK=c}2)6cj1%H%9_6!F_710&?DGZB&+M$ zCwSUg@PsGJROGAJ^oP3R4!5S_+pnA|s0%j9+ZVO}+%NdXYuv>4#oOw+G1n|>`HQ0o z0@p^*FHtY1wEDK)Ut*;lywXgSzrN}s$2E8(i$G#3bOnc&B1~EQAtXdKTVTcV*lJ{; zzI|b7vTAv1;Rly%MC9v*>y2^!%K=}q(nQAulrA4X`{F3QRQS5vmABu>OGo;5RQwW3 z_byml4#H)Fuigj=p*-X768(_a@BgUuN{DspIJFiX9ZObkl<~Ia zGLO-jRmVz0Q%7Oui(mX>tago(h^{?mb|!lhBicQgf(Q;}aPGLj{AM;wX&C%Rk!@ja z7Z4?dKI^Ux+$8PK(BbEMct>dc$mPn4?J6~jq&`Vcw$nHg>U~-~)K&i_!+rkp+2_wP zI(RE0`NYJ^5F-=J{KvBS@7P|XbtCu1B@Y)hY`Z=D*7Ze5UV5rGZg{Wct3di<$)WrV zpK-O}{uH(O)(5(U^I3-ytc}0YHVem06Xnef`nEYt!NL;#nd1kNMCw0veWn919Y40P zaW`XSMatPM$T?2t*1f&MtZ;Q_!uegk4)3}UH8{Ed74MM zjBdBGzRGahl)dBoBQ16|y=?2w{!tg{`saz8sA7(|i-bpdCW#A0J3QlmP_+!1+QrJA zxOV!7o(U7F&cNO&Bewo)3}0;63?ry3?~jlyED<7TDqoV5PQ0vUHq|}0AVQ0%&iZ0# z8U%JZ9z{VU@$993gR?%B!IGRMe#Dq0#VopVI&rZtt&TU1u8|_{v&_DFn|W`ieVN+A zMJ>hUxwu*Iz>1%D&AdoZhK|^07^ZxOw7;YdV{RA_q0WB7?@9AY!E?Hc0SmSo7b25( z9IOgHhOX&d+16Crs~Pq#6hF2Q-o3y6uJ^_z-S8sp+3?Y_xcDWqr5}~+$2nTA`Q${D zic>xesY!pYjjO+sdN1_&3-$Q`U(O97-bkyZ-0AtbAJ5)--%HnUT1n}F$z+W;iaS1v z5Gi7LHL^MF_rT?>qId26=@WM-1i79yXBAJ&7tzujw$Of5i`OsB! z_V$bSUll)vx}Y_F{qeg8B}`dIRKw$)2u!j_=T8)uh~6*ZyctQ&S;QN3!9#rEl}|}- z&iv*jg^qgPtay7u8i6y_vNKd#x}#qA z-JF>57P-{XaVWj8_AZ}@;D$nCg-T-r$Ge8cg)Cl%-djut&0cN^@cbXu@S__025!{H z>(9JUP|dJ;*j<`E_xy88R4%+JS6+SoVlDR_(LQTq`)27P>G8^r#&m%*;vTMY!GmCCD;ZTHE$BJU7nS z{=>&u;ncJGaXeKI(L7x&3)D_U5{E-VQ2d7cgmc{4HPm+U7J`xu<@@Gn6Y5fSe?nR9l{~k}?N^s#71%yaOUSTOv)2uh?z}jUR1T(Ic5`@=Cd_-KK76Bs zaC@C`ZvR<(e!v#FXJYWRQ+v9n5d;fM>_>JvIkc}Ng}v~JR8yBt6ekJu^0|~3t8Gj% zAS)@b`L)qn?u6)fDss+9lH_`l8LDE|v&E`Q1-*&y92Iq%Z+g7c3;kNT9G4+(DqZk3 zGCaJ#zm4+Y1f3Cezb}6sg5v%2)i#qmGh=JV=iP|cMvAQ0!k5(=>lhm2Ig%Rb?MgV8 zl$h2>NRNgsFCR@cU|$NX6uivV@&N<)W4L5@%Ba#zn@okK;Idq)#fTTWS?g=&jelTMWGuHkN;-LmF-lYh5hzoo=jUwYoVL zuq?ovJ{mJ~h2fil!;{Z*&uOhfoH7_`vDei7Mt!>91sWw?y$D;VGdNu z#Y&bH#wKZlM{g;APu}q7v0XLfNzXHb`|aea>Ra-%5jHH&S=}!hjL)ebR}DWmirow%O+zA=8`(hfrDeqz%c5q{lm4F#S+8uI z-)3%~(k!p!G1azFz|4eeSKR+%_+^;H#Zw3ormwT2lu)w#ppNMkom)*IuJza>*&I<9 zMcRw!vcL0|Ow>iRp7^5c>G#pGf0jCKl_0QCE~O}NtAwM|F|W4yQ~mvCOu8YT9 zo;~nYJ9_WKQ-`>k7?SO+@VAK+&U;!Io(_$4QL8EC6ZbqX)idk& zc?~Xm8lxUxO5g2$!L-alSD43E{f?0IGkPwgi#~yFt)Y98fqeZV*$@AdcfB+}U2|yQ zd-BmCB40b={yvMuJvsIvc;$mVp91!Uo_reR+T|Nl^tD5I-n#=MHg9)Y9fkYvm<1{p z-&*#!X&|+DkV!P(_(7a5u&yx0c5A)fXi$Z=MgO_-wh|F%b%&2Z#RE8V#9Z}?t7~GI zY}DB=wJNy_-!pJ5^FsGZju=R3Kbc&`qEp_h`98bJg}WKbQrLrSD$F#eso!_33@nOl znUnL;PO;fA!q94F!}`8HbrDzk)HLY5=u__a%Elb0BHsLNUO;J&a(_)TIKnws`M6;< zhjOO#6Gr&0;QVyA3zAI)MHLe*Gu=zWaXO3lUbDYX?f2Sqyi8Lj;Sn=R%Z9j-Uv;j@ z@paAHw*rP4TlSP#OFh!;>o?yXsZgU|)?ImVkHOXH-U7 zTy5@b(+yv%mf&&a?-+XDoZ0g5{@TRlH?Lh4rN_DnTX~4s-%)cl8wP}S`;^huT{|x} zL0LGwMHfTcHp}*yI^DQ5xbH>V_PJF>N_^Q3?<479NZ5ofa#YKjJ7;w9msH>W1OI8pKAxM*dJ`Wf!(1leJf7j*;oGm7Tr4 z<+MTUAnV}zxTlwF{H~=Nl08_#O#aOExaMJ2IFc5_Tgh7S9!?@a3h$ZDzD1pSs-Mrt zp|MBt3L{z^v(oaZsVgnVMJmH;T8iynlHi5Oydgw4ENgyw+X#_CxR-+K*=aJ~K3QY> zzOsv4xXx0kthy5cm?>SMbmI+{Gn<2VKBYOEw8c5Q3VwY?^ieU;Wgb&iCn+1=C0 z&yfpxz4YYs*@_%AoyLrlj1roNg!)d9G~vg6GY-S)YqZ{#Oz#QpBBDpavMR#zgq z@eO60P!LUf&RN#;8EsF(NiqSBbdGF;F=J)-L%Tj+8M&+I3GOht5_r3Pp`}A%2w57@hB6iT7uE&^c-lirmD1w7y@$tn1n^$j#@68^rMb!RaIP1J@ zma^wmD>C@1=#d|tl3ZT67;o^3HmXnYJBJ$OGI!KgRzQ>kt91ywie=k`hGj73yTJoy zAsVS0jaK*4?$9t=ei)btYduuuJ&^aVfA@$AY3di!GDEjd0e74#H^-ism(Q6fSCajWZkn)-5P(qN6lEG_=Yiw4Bl=rAIy?%CDUv^!hhW_&}MKDLLU6zZ(2PIOyg zPBg9(*LX`T=28X*wI_Wo$~%ds*FCS%c-{7D#|Ve>h^}OYa;knN-^zWsUHOyQBjG-c zw}KNrMdnGZV&k3jWN3~rU$b}{wV6X1-^%gp% zCgoe^R%PyfQS2Ha-KO<)#fR4{KDvs}lJ9E(`Ud+BRcW$o)4r__r+|^hD?WSTzm2uC})7ZXmEa_RcmZZ6En@ zEb^T1Wz-m(+WZTZTtoRAF(F??Rn%SJ%qc~#BQHxfvUn)_Mcz`L4bckq&v=$#_4S!2 zdo69_o$v3DO@xeLJv?4siXE>!IiY>dS8d?F_hxIL6%|~>SW$hxnUm@(bAJ6+PwuQg zBIoIFvVND$(<|cT=uNk)CxtaE_u7iiN5J>mIu*_x5=sUU~O@ZCfXKHsDjE#6A-eK=G(ZC_?+==ZE8Ct2w0*~yosMIA@1~Bh- z^n_lV?|s}t(8F{XQ`Bu!{`51G#g;UG*Hymcq_xTtN)xYbY0DSef)Zgn6j^mAd1I~2 zyCcSt|?5S62D!Q?&?5!JUtT zPHJ25i;Dd>jxWM*VlQ7CH{0D*TQAuvs<08Uf8UuYCL+Q#=i4sZamgd2kR_NcaQcYz zEys^XpKBR^cVu6RJf8b9Fg^N~u=NLRRuKal8 zW5HYdhxmFb_xvOs_%6)2SF-F=wDh^~?_SJI`2T2o=itnO;9WSjlZ|Z~8#~!eHnzF3 z?Kd_ywr$(CZQFL$~5-Urp7isqUURed<)r>GSl{Gt+7C@7}$~N?&BQBMK|x zi)$)`jMoQjJ%w!s$!4N*h)GX)pZP`^VE(J5X?#GRoJ$oXbSiISkK z)9M8Z3XCuNH?=PG&}`M&Yr%qZ@8>5Mqv@k${laQPyX1{qT@mU|8!`dUCKA_^uyIZc zIqLbbZB|BHXQ*Gn(?pNh8k6Hf2EQd>tjHz<=a|TAF`i$}`RAvfbqX<)`J(D&yzSpC z#}yZ&PuY09!*ZifnO+3cBAOf5M9Mmqkgaw6&&CJXl<=&|;1-Wt(d5r(AaWmr`3n}G z1$$^Oiaw@APcQN0FJwucY&l^V!mGAF{7$gW(7(bzlQW*EwxDdOKaphJg3vL8=hS?B zlY@9hh1dl`=9M;D+t0#*dOlL`nLRwnsv9-y%-R*9(4c%Ec|Mc;;`%%bXv6SSWjeM{nECAffq{;UPRmf7_G3>Fi?N0_8)_=&jM!Vb zsLmPcN7(E66%51K(3oTvSO#&!JXx{` z!g$fqQw*c0kZ&7KK49tf6%lpSUpSqm8GjbKRrFd@do z?6;f}mL!uE=A-YFyxU^=vP_`TPi<&u&4SP!%yeZxj8vYuEhrrFdkXla)w4}>^Guei z8@xTm9sW#~JJFogL*&^l_LX^1O#vjlTaEZKAWI(pELj}0)-(MDY)fC-nALS#ianpx@xDUs$y~3Ipub}T? z%kcn)SGp0v{qu=ye0S=HIcT1h3~l-AOT6|E;1zc8qKFWG?`Ctb$O{(wM8F0{ zbb?x8#${CNi};`UO9G2w)cav{gZdNcZMK2Y;>DWCrO(NSAf=deoUsBJu9HMGJfGOkW}^(~Th4=eQ{pj&1&3WXv}6T~#_qGNrv=VC+WN8bXO4%T89pTGGL|QcwIO54?t9Z{ZYRM`t*gn6TaAnk*uL{<(y7eTdlnS z%IL^J3!xb{niJ{|ccLc`L5ng-L8HBhJ~7S@T2I7N-nOB=f5%j)u=5Pz^|!z=QXz4H zGP1R!#Ph^s&{|i!P#;HMaDAVx{alA*%i@{6o}0s6U88W^l#y+k-Bcx2j?Hv=I?H?L zVc|d{v1*N?1a|M7#x;OGO?x~y`pn`?2aCGuPwt;(p>7qLaSjc)YZB-fwbHU)5^=At zd;yM~`m1dSiIJ7zj)I%PE4Ucsw0s!E5J@kDfE15`20A|*h@z14 zq#w(Sm5&RHX~$4G+ZG!~8i_VL@z?k6UM$?lbs*(`;Z~AxkD%aLBUHc`jigC3kU58k z`F1F4m6H$>3`folBYNQsbRp*^D)7hNQnAb!oACD>HBjw@%MYO1h)Sf1of_s-#@SHa z5GZG@c2prbAcZLZ@FxBh`7{}^4gLo}@kt{zQB!HR)Zw`LdJvIw2k|&WV}O0&sHM)U zS*mi&hFAxoTUf5KLL z!cOCa7dpQ8?w~J&JjK!*nE6&1UnHx4<<;KyMH zb;$kekTl~E&-k3~LQ}0*6pkHNYZ05{_vctQFNL7Zwy$2=ymoO(wwO-Yc&rYcec_gz z4fdcFBS#{1Si&RV4*9_~S<=L+NUZryWjO?9f}cCILSN!J6jr}|m+bF-(TLAa{V*1m z$jxR+bJ}SJrC&`EbGZ;RltsA@m;w}Cc8(%`$2_2fe0jT(x6NmTcz`u3_ntUs2e4NL zH!w8sWas5_T>jWu;-{;1?v9q}m-CJWb48g$64AUuZW!TwLa5DZIt#VIbOZTF#vdxw zX!TkJ(}J||r1R0m{$9Whh$k{lvt7TYuPo~L+`Ftd`w#W9KOF)hw|g=j-Na}B8<5zpy7^x&}%CLt`fvup7(mK5?+QWra?%(;<%f$_@32{H6G{Oex5%{5`9TeBnO-w-bAa2AgQk9Yy< zWWvZFaS4;OxPd?MA<3lH6~e;^Ql#fNRJ8(jZ?d|(>UN*zii%*;W70Qg15FNA;aP=V zxu#G%t9E4rHc+O^V`STR24e3xEqP{d@1tU#7#Un;UA<=DV*%%S$pz3jxtm74^K}1^ zShr!XE>BoL&6G{-F$gJZ4MJlQS89<#r~au+WVz}2yWd6oVm?^hW+OWx`oTN|R({d94Hin=E&BWi~O(yjYm`{Et};KpOklh(sb) zy@GiEN(l{0R{9ae8%$V~uS*OjE5L=`cKe+h?HzYxO9da%U4nj*>v&H*7t2Y zLGg+5n&pYSDWE|X-;(+|4b>2#7)M=`@Zf}{i7F|aGsD=JsEMPI_w)N)-NZ-Od?ycqlBmF>&kr zw2eU#5UfO=vitu>xDZR#YTrqjw!DyJ*#Y2oym%W06#PoCrNe!p%eQ-~T&mOZ& zo)bRXv1=I|~cNrN08KLg-}Km7j{fc&5F(TAsjcklqMgtXQ5J(?%VjTY6f z3WB~>Dk-Zo>@a?WU8o+lY|`3g5-SdAt3~y&N(qmJP1pgvp3d#j+xeX?Kf4DV)PW`? zilo`m+rphX5`1r4vNv7Hhfd@H=-$e!gBL&#|H`O+nz*5yi^B#)6<(hwXm%p3v$NlX z?|4t4@Cy8kf}IIWGx!&-HfP|xY<$YCQ`leO7K|Ij9S4#QC_CMxfwEfq9Vk1vIZzkekF zmG3r)TA&vQ0I3S`_0PYR)}19M=?T~u7dy*ucd5YbQM4tvhJ%04NPJJ~vqOKvbb#Z0 z3~JBc(%jDd1^cs!;0~ne$T!Qc3)&7wn>%p6$k%FTt(M_fNUL|%?~8cnxR=5ICe4uq zVJg}SM9@BKfKtefX(v!SOoKQB>ihBXnUQzoilwX&0xRdqpJ-cXP)z1?>05A8{=@Ze zhGOslPj6u-$Zl$mmK{Q$Nl!PZY2XcEkNa@|{l9JuJGvG^7(tj94ZBU0E6@o>5dK^~ zU$)-MAUn<--kwHLH~t;ho=cHeP`rvBynFC2gpB}{Coom3e^`B&zD!f#IwS-);5$pC zL_OL5o)CxG6t(BQKfb4U?_m&W(=p`U!7qXC3F%4AWmtV>#_Ij6@76S65-{#xgIztFT91LqkNR=9U6P;5Z>>OhQpnRnEBM^U!mUr<^U7uLmds=FYqVA(;(BDpo7p9$lDEc;mo+ywXvwqUq*z&x~~ zy%)d?p#>7rC#_5$_x}vM(e07bL2?jw`}~p9k9NWaW11{gfxTe?!$lw9;-?kx7ZixL zv4-`kQ^CJOc)ItTG-nh1vH5{Cort|f2$(1M{_5-ku=sd_^$fMBZYsgHiS5?0H+LYr zO;`RNJ$g~v?Wso(!&?1l`L!eErv~TM-7_6_$1C>X?e`3y!56)c4j~}3GYZCp`NGm` zTG>+$@dVrPYln)XRgC++1rXB}4FNCSJqk9B;uhCK1<$9rBTD7)k=w%yd4=)v+m8tI zMb=LZwgN_*_J#tCh);NdX-@+z2JNT9PB>uxCmvj!+}>k0O^wPT7^ene=mf;_4FrVu_NJ5aGcF`?U}H*kKPV0xrCQpzTB21nTTO>D_h!GyN5R7O))plUP(jjHhUr>9q!(QYXUI4Ha+ybOA$U}WqMotwB z{37Q=x3NV=mpG_v`8nwOwLGdkJR|awI9ok;91`owq&|JRuI#eKzjL96vr7t{>@s0tZ>u)+Q=AHw4ki1Pf2m= zy`TX1Ev6Qoq?3N9fBHG`hV(_Hh$TQlIwu!yWQbDutd2EX(Z4^UODuSsravZ@LEjYd zSN!h+$O|h5$i$-(%?roZ6FP}|(@BP|f|49&;04X9i$!@>;=~=z)1dTGTPI<5A407? zw&l7Lg^%ms-{LA92*UqnCV67>5?VTREoj@p)-P4Te449=3YFWubIPN>(=`f>!8MRb@?3Lr~0RrGCMFn3e{F zG#TJ9O8sE5c46t}$)dXpH2{t@ku&iqkK58W-BmrAqQdW`dgo1B*PUu=NoV+>?`g zCPwmVqQ@7!p#7cb!Dp`1jeb!Jar!|wOk4w>UJ1HgdNogdCyHfD8 zG%b?)1O=a!6QIPxpI$khK+k4%Fy-HVtijdwtn9rR5NK8$qzS34Dpskx)PnLBmSM8i zI>S;pb5s@DF#9B~+;N7!cP^@g6DQ!EjeUrSg6=e~9J(gYpG++0%Iw7FhbHLCPxUnY zcsY$Hm);Pd^5I9qbx-!GDj&O^LYAZBl+_E(cAn6xYKh>3*XNrob9!AF4AZO`L6>|2P#x{A9f;RWNnY6@V*FkhShWkuGYDaO~es(&w}TKS0R@y4Dye_4Lh zgI~cApbBxr>omdnkGI6mNo2)hzZk*Vhn*JN(3J`!Fl3AmIuafyVG8?DO?$@J78JV) z^1@&fB){KAJgxsw?Sw4O%saOxS%7K8rr^dZ8*gaCGer(2K)EI|OeAvwVMsTgxtLk| z!kVG`rH?qW>Z3LJfVaU^=szG5AjU7zK7eN3&ktUG3C#;v^S9ycE39p&KsWp^E>X1p z>+w;9By!&XU&LQY>fk<8#Nao~J*&RA^?0m12ol>HwxNe?aX!zRAWj>P)sT^1%hDOP z%>%Djs9wlmvW8B7*7PAV3ilt?LHAu_u)qkBR^YL_x!Q2;Xm_BUwWB$ED0}#QZ`Xms zJ;OQJuoVX6W0^C00%42#yimFOdTRfmbMw6lMQ=)pgqB%W>}oiKh9RfO0uu6VACX0f zpy1XLdsg%xc2FJyL}#MjdQWjgXX=AC@%!;at|h(SR>FJ8B-8Hi6MfeoHSiP8EtP`ZYJ|5+oYG@`tmwKoC?d$P}&Y&-8GZHQP3yRy&Sw0lFEBATazHSJ<* ztX?tv(Z*rgQ4>DSLpwcs34ZH=UnxQueokaBVt_)@cB-fnmeY6i2WKjX{6LLvt2QvP$QOi}Z`#ePc*&_GSPARcv zOhsKeB1@xY|3c>NRG7Hb)Tm1kQ^HE$Rnc%1XL8U$eU{;G*O?XzSH)t!E^eQX!ZjM$ zNQb~X8l<0%EC6c9T$Fh06P<}`UC5rOTUz5hyoq16hWgHI{<_2%)jVf7~-QXk!6M1+pg5i1)|Zql@9`EE*UJJJEgDEzk%| zVLux)XL}QZ+Ot(CSzjq#7*42GDGG|8>yOb!Mo~9N`;R!8HC!?Ow7IgztS}!dlDiVV zG#unt{DkBK45=!OhJ}m%MLo-M!J_@ZVbG6@&D{T8WH4q+Jk}q0<%pCRRNl>8>lxi- z)QlR9&E#W)>dQ+I$hS!`k`~T8m|hd6gmN$q0|wc;YRNzPfJoCGTTJ0!LDiyUjULEP z=|bOdV+VWM9bWgyH|lx`DN?b+I}@UpEITbad--dmga^}msy+>_9Bkgfy0;LGlq|0T zT1h>Q3Jry^z1jn(RRv?-cngJcqAZnLM$e9HHj+GJ0C*b8R=K*TDUAm@A#?IZ6Pv0b zvX+r}a7!8@q-#aXopODdsY6)$MXNZby*+SG{yb903yBjp@u_%~_6s^m{DiGEuG1VN zuI+bVWanatkV|a+^GE84Ra|X4Z$}M`H~rsZqQbz^Y^37eoLDb_a-=cM0XVvYI9%$pL5qg>(`~RaI3_(l<4ZG!`Z>Bm}&+wzj9% zXxO)*ft{F&3OZ-jtmpf1N58Q#Dc@0JFe^?xvwb^Ifox+P6z5rOt6p5Iv4w5+> z%+4PlFdv~LW_&mMTk=4R3Bj+|;Wl6-2e4Q*n;Cd=7vl`Xk@mqUEqyptQo#Fo^%qAh zzzHGZ=E~h!x~$d~*J^jwv=Di&WW(*$k%#{AFf}XRQb6Y@tA$u< zOf;q-MYh=RSG&!YU~YD{kJ@X-KQ0YUk6CkUVhoK3w@k67E)b)xELc|1<}d0D(~p@J z#*_us(|t)j#n4}V!YUt(N4h>V9B;fW+o2~$Xg^g_P#e_*jtqXG;`;`~AkfBykx{|sCzHPN?&NLVVRE`-9>&usJg zeDkqfjHi-AYEunbE@kdz_xar=yNHuuY$ckE{Lw+Qc-M7bf2)eLJT0g$=zW|TURti` zo^771d40##_p6`|PW)EOfMHUX_F(PrJpaEL9v)jp4h&2uXN-ASv*2O|j{K@hzVf+5 zIkrOG(o+4JPb#xEII)1~EvLfOwodre9jkpK2&kXwVNj;{GaNs)m=lTfDB_Al(af^I z7X5K@*yV(&xnH8)!wsYJQj#CM0b7fU96}sD5a@sQ!;J4S7%2Q58n>dC?iz|Hf4+$T zf>W>P1jpVQ8ghhnAoFjqJm7L>XB72xk#x(;17grLaL0sm@E zAx?3n7L6a+ZouL-oYk-Cr8f;=R?8N1Q|YT4La-v53k7NczA&MBh{d|W;|E+OyK?!)!dbA4H}P`9C9{8E6^ zTeq>fayw`*qnv@IhxU3K8Oq~Yj6TEP5R$u9+vn3QcYAzqoPZ(BH6yCyV^8sOCDhu3 zWlv`LPuZOSz{#@zczxVD+Dz4(i!R#z&}svVz`z`In0~ot|D^s1+-D0EDL$={Ggx5c z@&$E-{y>lG{ngM%h9MkhZ&K%Vt+hx;$&DheXVNL^)A8+eIp%Y9Q(A-+JV-;w)7^Ei zExUkS39$3vKh=InNgLeU|CxXa#w7U6NF&>%vn zxyC*nbF$mqRaP7AHb{kk*N!CHhr|s80|{!sb=6a5&m<}EkfzXm=#ece_tzC`5kTZq?o46vG54@tQMKt{bMbTIrFzt?d6h?^LlPD;u&Pq;sO*-HhqBVN zP}3Q&Q6b(Xg`on@Wo5MS)p_6V5^J4f-j!?jf+%F-m=~%-Y9QPQe80jRaP1h5wE(oL zHc~!85zY3Ve7%oT?gPE2?m$Lz9?_5PxtXd+U$XB@zx5Xh9p%l>v5)Fz+%8%=TQ%@9 z;S9z<<gu$OSlWEoAUbJ}t7E?;qSkW1>)nX@)^;9eXLIbj#x>G$KLghgts zcDg!BXwg8QxLdwclWZ{2a#3A)i58_;3k+84+PSeWe&UD0E*aXjE6eBK6x13XFnw`x;@T-yc~7N6_JMhDxhI^3eH#i*+-T_KwAD zU&xjslO0833vEe}|GWFtfyF?sWqS6BjO$4!tG2ql^M$3j^ry{Sr(=3Izfg|k;G+>ib$B)~nMs4w>P3^P^_`B0^yA zm8UobhEn;{X=tI%BnzU1S|Aq*B&6VqN7f1bUr2$+4#ls*a>A`Z^r43ibA~LDY=k%P z&k8sRH_I5rGn;ed%P6p?pfaLUA2kOS5-38ASZvm`gcL$mnLk^KH8AP5Z63f>|DioW z##%=m+m{D?Drmv%d^Wk7l$~O#rjOvVwi_EAMyZV+>LzwCZVk7yJ?Sr%U1yd_*L@r= za^9TYTY>m`OCuf0OLotJA6ixt2H0~n!Hn(;S*tC+nHJ0J*NPHB^u@gOw=svlP^a(G zI9|VKZ}ZB1&u%%GKgoFvDg_T^p}7jzn;X}ekQ^y{diIwtb(Go>in`98cx6Q@KHgN0 zem@*)sI6vXx-QZ8Gb1biq*M;gkhGg2GuK#|{j| zB<4d>`?+*&<{O^5@vj$)v^>mXHX9N6Zzmhg|xtxrv{rKQH{u!79& z4MWiz*2Y$R*>jz4yL|;Z#h4@#!*f+g(Udp%6(E+6sJ|{JE_H7NeaW@r-4r0)p|tWz|nZB;SL=VylmXBT8sPH3wWa-pi#rC*+Wq5j#^m=hyG5a zN_w?kw=V8b4UL+Q@e%__Z>8%QO3VYa5?d7wrv+Pb(By3Afci%U05?G21=CskPwmRg~_ zWwR<1P_Mz=J$er_{IVhLwBmdZa1s%qc`hZX5mhIEZiVjX5h9vmgT(DNG%_mubZ_Yr zVGTIN8&!u{4YcZ(*2@-3Hp095X$BQ+#>|dXh-cBhyFoN&w54xAO%VQtxQY*TB*6_; zzpL*fspEC?IqN#1WP$FX_gZ_lO(aF+06<`+)EXD1Hc_YdwZPg#jUdwmtCfzZigZ4202w zQR?&&zwa7o)G+t4>#*O3h{a&UClUs+bG3Cz8H9e3&w)jXkp{ZmqJ@#;qDJ}pY$S#m z>*uJ48u+hv0)Km(v41#tTr2)w3)enjoHn8~#rl&Ic0l_I&k!0pq`rG3VyyBpSNSYCukXoe+nQ%wCW$ zRd6al9mYWCxFfxpi4K5fi?7fPga5wn|9WCd7EZ+w@KSDCvV~jkJ|cBPZrn{>T!R=jv*IiCk?wGKnAfg?Vi=Dzg{c^2k=nutDP{dz_@W;*#WxLDjpHbTZ( zsG8{-^}AXXczh_*fo@ACAqaRt6*oHo^yuP!p?LNryeIEvsnEtGg>ZGCOvAi|?7zS8 z!9KC@`@7X>9i$Av!WEI|YgUGN&eNH${v z1NL`^!nf;9g=*)nH|T!FG97Y3f}DafoIGFRz<|IGf{8VhK0IH=Mt*g>+EOl}@(*m@ zM|bg+?J6|`yBx!F!K?rWPl6Bp2v$T*f?1cq`R8q*>pRftz5mJJ{pRH!IPSB}Jm>fz z>V0iU{v>#i@u+`123?ETiA+rdd$j9?Z0OtOYxUqQ2V0qlCmHTezHM}(g!PX>l48Ac z3pDD%>c-9u`se(74vblw#l?Gv2^VpBJ0@8o`>1A+-+Q={=8-V(0}7a-o< zu|UFW!xz!)d8Y&9ughcw&t%qL?*-I}#R$zl?`Y^C%ti#*b&h6p%{cNCkRTb@Aiv^1 z-wTgNi6tj}o3Cl`6$0LQS5i1PYHi5(-`fwMJ%*kp-xCf6F$A>%*D-qTf0PKda&OD8 zf%s5yAzQ_Cz!-4c`vhPJXd=y4D>5+?!Fa-JE{mo+LP%DsgS)Jrduy~q5EAvsFj2Uo zCJ!evd3k~8Ef79hG0QL`>S}yis7p`Z5F|(D?GOYa2WiBRLqC8KLrZI1@wgFG2$GpC zzIY)AtCEMOZh=z;L}>JI0z^hWh<4|MTH3o60%XR4!KB9>-mWCx5>zRtBTYL3rv_1( zq~6CkDji<6UkJQoph~^#!gI9-Mia~%(#6EBB0y1iyPY1Cbv!aF0Cqu@N{tF+q{s}8 zt6U_jQloK7Lsdf`&rb)Y!oFtTcUthK7P^UaCAWtYf~a!`ohSp_*BkUgaX6X)94~R) z+*2*S2V(vwNiza0TASsQ*L4%-zlVIlT4#)uBr_Ly&o6%c*EeF28OA`cyg)Y}cn~v{ zYW1TA#)bN?-GELuX_xXU4S@Sz_-0J86!~f`w467GL036!O@p_vsrHgj$j+j{$p_FU zZEt4ggb4+ogNc!4;?BEGBy#t-~$~i;Yr3k+w(Llk4{!Awwgdi8edpjt-5}%JZs9tV3tRv-ycvyeJITiW<^}n z$kBIBG0Zrbn?KN|ZwZ{rE33(2c1ASLEn~0GYsw_!_YEhx;DwD#aXTq!NyGi#kV$lk zZ#Gg_cqla_fsaWSZLnduFr4m{R(G=h+B8(0jt9Dr&7k}+oT|e~pi;#Z^pK#mMH68B zv%r-+0b_NKDuaOw49VQ0K60s~*O1C9jhMVCt7y%`YOM%;FUs1iG9?u+hfjH)-#nFw zVllju@khKjOfr+015`=6EC|#fKjtyGP&rjJHX(R?l0IvT37yy!xP%*5J|=qx%BN{L zBjb%Bk($5Ql#YdKdV!15a~!Bz|B#_ilP+)-^fsxhPK@AmKq*f3CY*XrA2APgCl1Rt zUaNmPxh&OCKxLqOUpeWBzP>4=a7>@W!!t`6Iu8})iwkzKYUkw_M35vS!NYVeVo~Fm z-#B&e{i!$8nL&-HsX+kKd!!;fstP@;S#@7;boHQ-;SU2C8giH&@P;+m=ngwY-GG|G z;_tA4J99FVyfljEgIzmOc}764O%!!=msd?-Ks1sPN6Fq(nWuQc(42R4ZL=4hc<4a9 zLRQ7%t3z|+`h+`L7))3e$5Om4qdT2kD0G886qZ(`VjB*Ab*wtD7@SO@52v=1+;!am z4^9@#SerW0AM4wkrbSQnLT|@>h`&Ian}1(*ggrQbv57Ts#7n8*g{R!)O_j7taiodk zMfTFXQ&7zS%MvsyFFg<&%&miQmTQOq@fC^5JUTLUOFqwh$c1ufAo9!|GS;`Km$Jey zxn8>tBN>S$*bLK?i6oMBg=Q=>n7JpjGC%`!XA0rxzj4u+W&9SLusJf1CcK2cQQ8FC zM~Kdgsp-}d_@F1g>XCKO{scy%dP8v1UoBmHoO|LzZmdiYA#kiA6Qts2-rBUI2pfy4 zuoOT;KZ*_ulf0?c8M#3lZq8~8gKn}okx|6d5^E-bCc6i88c1ETvac+R0y&~|`b-#^ zpeM1OlDcz?6(wfkOvImfewt#8Q3m{=`CDbX=N{*2yqf7BDR$&T<0?}CK2G|Osyw>I z4)=SLTx4kLOq95NWYzQQk1^-y$|QlVdZ0j3-F=K=--2Ao@72p}SoGB8zfVLj*jF`S z)QoSZ9+$Xh{i*Xl)Ri&bPScJqP0!_!iC@I`royeJ&qpoQW54nYt0@DZ!p}dZmb*Xx z*qYP>=T!CN2QU7~i3a(0y^PRndGm|5LJV!Kd;ayZN7ZE95&K>CI%w+B6rTa?dTkKQ za;sRQXg2$JW2Q%nh+(?5_pEoAyaS7!$$uPK+<$Cdx`&Li`J}V_9K3}TAe}Rv}dFAwq>u^e@T!o zV>9V5<1Oj9s%U4oT+x4{iN0L#XG!!LU@dKJU|LO9prK-B(QXmYC^E>!)OyQ6sIR)LF@ghyZ{C0(pLaCmoR|dy{_(P z4%_*S`dcpbY#b1JRQrJR~4mQ4RJ`A`^906 zlYFHW;^fr83`f+EQ&7~06`90ZUl$l1T!v9WPZCs7mOCG#r2+9E4ar^wPuZyQ_A;A{ zL8_=P|D!viHCaz8>I>;G!&XFG$xv=wB4B(EzoiNKxhI&$f)_5-K?fpv1>KElH8MB zOq+uf zTIAa4G#)%B;dSd@GMTIVEPlv9*v?3QocrGMY>>56?WDA_@{(?B+U&33=8wW+LED(C zQpm+pES{A~k|dEtt*q#ISACo_r8q@)f@okB!1i6Ud?CW6rDSusXuABV>9Jit!?BS9 zO8xNjy;tZuGc-s^zJi!4dAyp4>A_ROt2LkEHo87GSx>u+2(MO#NuuL4?YGL-ag2@i ziC9vMBuPRb#!3%>V~SI;zWkAY(S($EZaD}f+bYS72IiJ;y@ zb*VUuXh$oZ9`AC4C0w-_T1#qGEkIWA^Q~5}-ShV^;~eI4OQvgC?dSLelQZOuEZM0} z$B3^ZS7mqv%Igwp+3Onf{GxDmxyU{kp{64cjIzoJs{XW|<=|4k z(wO~}ONWv20`{4E&F5gKYn<)Lxe-h7i2cW~Oq;9CeKR!7L&=->_y&WImb-<{@modc zN9*&sQ}=}vG;hg`#~tjF=_3^-hfj1t(>CjE_mtjwL=EW-Cx5RRSK!CQa|h8v3Glr1 z-re-d;%kqnJgP7JwN*T@J!$jnFN>$1)(dI&6~B>~)n}8gWnG#d^NM$MNuZ&{q5whd zLM3-m?@9O#WkJpkwAM6wW?@B7ahBRxx7%WWx?NvN7FXq<(n-89ASYX;7smFoR(R9; zGq+Qrr%V2kZ1Y-*k#eAij1EmyA|7qNzbtH23l7tOO;zn$P-#Z}W0=CF>KrjUYyV*9 z*wN_-78CvBVIXN+UIlBba$GzN4|^~vn28ErPRDDso3?|o=+%aAKiC+W{pXgcgNt>= zc}b>8Op9T}BsGF4&)^J2%o-aWR*lkS+L6v!$-hGc%(PVY@F?}G+Y5ZG=TZJm}D$-N6|n?A$8LlgD7$N0j!_|9q5>+h%1 z{$d~b!3{%1pu~cY!CwYvbnF{QGK`($aT#B>txXJ>_iPXcq(KCr%z_0D@4^kg5g^Lf z#^bTRh!}GyFum1-1l$J+UIc|N&Obpfe?g8;7*Dnv!|n>X6Yzyqt!2nYNkOs3(PBG( z<8*J;@QojF#-~m_mh%VaA`#-PN)c)Mn z85t{dNemy#O0+K-ai-21k-{Ps5>Q4Mih%-1KPj<-CK!)!d(M?Iqk!cwG{zJ{dD{eD zMMaL#0vR!itASy=JZPkiJtJ}?SgZ(eLta}z#-zz|yJa=Hq?g_U>uRPufXy~L{ub}} za;)sF3F`6jkz27)Q9}{~#L#PF>bj^n$y)dVQESWOb-N4#xGVS(0=yxr2HX&R^#fjz z|3~oE548KRRq_AU{fC?=8t8MnSgv+9Gb4ZURr4oFr&=d{7nm;@lh##Lk-zw1yOb7! z13!5OU|ZsgREOE?{!0i2QORyO`He(YDWDtqSre7?Uzg+*PwL)dG3k~}XA5pes_X0J zRn{C)ZnioW({d$`TTAGc;J~Lh9{u_J{IqD*t{OYa?vK-vqhHW;jL+it#9ZUyUd3Aa zsEe{HmlV*(f|uyZenMl^hJss6yoN_zKY)OzH?!RW5+f8aPKB54mj(|#x?Wnw=`&#*cVF6p2m}^&$`G_WtHDnLZ zGZ>v!PeU9Zmy^1FfHMaX9j z;2rn{@~t9!vVA_rdg}jg&35#6J@U6%cj8m~?01SAY!Ry!bzEP=$0^uAaJei{g~^m6 z=`w;p>eL(uC*zPu6Eo&m~obdGV{W8@73s2_$=N1^U}fp=3aeCBY$bM;xX3hR;hj{ z*fNK^sB^EkR5k5$-eVp+wWr2B~H0TXJ?NjjbQ1>lm=z< zoTZ{Y1#punru)>!2Q0+0=tCCJJu=;xNk$*Hq2$Rd3V1zd0)E|muR%$eu9s4UNUb4T zHyKzu0g;Gbz>*?Md1|$yZ^i`S;>Aiz@8iL zeJ&E`97jE5MXzk37E9??pctpR1@I(o=1IeN&Srj^{lMGpk`T6*E<^E&pVpM#dr3R{ zg)HW}==t1mZ){Z}X8DeBE4Oj!JZ|pesPpebH)8vgW67sF@fGip=w7WGxRhZsS((v& zan1en_TH!MksoQwed(FGWqQNWb!XWj)tT?u%o6E%`A+~~aLRVX&3sIs^==w`yfG;~ z(~W_3;+aX6{&|8=t09J|aZ&QnC9ipn3R=vJ>eL$g)4|sa%h~xULF^*mO}m?CZCIwbJM9|e_{U6a?+D6) z&W0MsLU-qcxQ9kFIk>NQCyUHLm`216%ucExnGeH(C*544i(JZ@d3%?N&g0X%m$tgx z=5|*va+s(WoT+$HrnXN5K3}JOweu{$X~^qD!~QVRJr7VCQ!IJ9 zTvuHM=8ceXv7*PJ{8RBT%zwSWp-7FBL>sG(bz;p{`hO^U2O!&;CSAB~o2R=^+qP}n zwrv}yZQHhO>$Gj#{QI5x=FXk?@63(6_O6H(wIi}J*3QhR^;Bj)4g8_vZuZ}I9x9f zh~#35m5nK4ypvWhbJ6ISiGf}UpgXANA~7w5@x2ZwPLy1&+o*BiA^B50L!40m+Y4L9 z2!)-*xtjdcX?cFLZMyhsy4;b=I_s!296B@!r&{)298y?e%t2%G|`BgxS;5 z#jBZ3vIyc5(NIRV=qvvPj-zxeMn+swTk*E6$_WVofb`?v@>_uc*ph_Fg5WwZA`0L>7LaE~VRMnitP<6`g!K59MyQfmLF0 z0AxO=sJ>8_8R5^gt-R;;EQq-xQo+EqydyOuQ)W|7)7qpD@1`k3QUe1z61S!|F};L5 zRJm)2_Lr}T6XRC~BY0aj*^<#RN1R@Z8K-m}2xP2e?qwNz&Qd{@cNdRQTl@KN{i^L( zH*~f&yQ(*fB(Btk&*8JApN^70gV2OBl3SX zDhFdnTW1GDV@H}F^NLcIv|Zps`1+90MUklJ2Lx0Z0whW=CeWgQs{&Du(?B85{biP< zF-=kzhPP5pm)X__zXR|bp}U|fXMYg~EQXrG@E*1=SG)4}Vsd)&YTAd<^1W_5+Mou` zjI9)a#m`m!6RwQzg+C;SlT@M_jOa(Oi&-gvV9Q zZugTQusH`P2owET?f@ON2?t4#)e)yy4hI%fcP}GT-5*%45o9issj-?IQe!q7^6E%0 zvwUm=nz7sjswP>?EopCzTWTu19=P-7cmMr{piV1P;+w;Zebis?C~+|$6LtvlGqoY8 zXEXxys(p0$4spLpr1`M$Y?H2`sy^{}x01uy?gkj$--Y_TaQ(SR(}Jbdn8SNf2+S5q zK-!nZvKx32uCQq0w92LsD6lr`!`0szNyg4OcG)avA|26gQ4;osNo*8pjxIfa2eQuP zd;fwIN)gj(bQNPb6>hB6>u};paTWIwtPwl{GC)s*mNt{datFP!kKfB2h7le*htAmY zBeu(jkcqEl;?IEv%gb?7Pcnwd#s>RKcB0Seq}#NSTBpZ(5iIaG0!(H;MWW2j`()7S zQJssJw@5diWY_OBPwh@s^@(IY?FRh%fwr>NJhm3Q>Wa4H z^Qn()tLs}h&zg_9k8V`8#psG{V$*hbiHB{#h&LDsXyyqlb2@JI%G-` zp8L#1*NRWtlTgpQsnyWtm%8>vG8=?l&(YM&*w&`8E(0sNJTRrd6qhi;il?VQczJif z5g*wrzQO*}FUV7K^8WtIFZ6!q|GEBqzd--_^*{aMf1^qL@erjh8)SZjud5jsB&vKN zK&%}jf4|?;L#beMh2a4QV#!*Cexn>&%TM9&`%7U3lJM|+h&q0xxl(B*wQfKhM2W+b zSC_9=l4=(gHYTRsS*uxTU39ST;%6-Yu)I{gV0OYr`YePYtOS5ylaX&eOi?p+Y&1}u z>=<-cbr)V^E($9n8`urA#2wlzHR0{;^(9fyf~@9`6Q9pfI0+-Jq}9Co`ah`Pn9jnN zcQ^EqGdpa0^1NB&QexrMEtc?mQA&jYZEahSOf}PNrSm_yS@XF3;2~tDGVZRli=>XvjMG+C;RECNN0M%qV?F6)%`h%Y}a$- ziC@viP)#H2^XI5LO4n0L5zlB2>ujz^**x9;q<7YZho0z-_=rFtKhF{J))8VPB06Sx#&7~CpVNQ{WYZUR}v^C#?iRi?YmEUbr+hHkV0Q!-Ny9t zNy}A)`tidK(>0N3wG8`JaF}$83Wo2G=v?TRSapPt#&dP`#2dbCRrzEXF{Lt`qrYXu zc?5O@x)HgKJcgvbc6ZaqXSjqpnvpRGD+=vH`ktB_`uUb`l~+5%R#M`xeR6qp+0tw5 z))r)JyiD>N{xIdVkaUD<7s9Bo6pDoIMybBEB2m zHXFZwjQBr-{d1d*QrN$Yy7m7W>=Ay9n%~aOO7PwRp|ZB^8Xb!FbxqjE zRx1jNHCdhkA9G=I`BL?|psto9qzW!)Ll(=&yG{Z;jB&;aAN~-}bLRg0E)lviO#wQc zidj;;j+e-9IW5|taRr6;!&6cRdJb_LOkWesOd-U*B*%L zG?W#9BJH(tM$^m5;*lv0f1Nsg|Mu}(^EJKr_|BYjoMD?uWVsOUhn?s{ivm``Ng}jm zwMZI;$Q$zl9EWU)=3*f(MGOSFic+!f7GalN@v-?MLQ^rda+72SK?w_{j0a9|MqnoT zy{S)@Q&>w$N1nD(*dRjQ-ktGHCSd;-I*Hh+;mmtZ+i(m8N-mYF+u37A3d{zIHei5wBG{_o zt)N<#t4dKe-!iXR{Z9N0_ATe#$D>C5@5P2;uzp>SE@}8x1?hJ0+5FA4>JAeO zUJ!NKAj#NVAuJa2Nlc=Xf*dX+mMiQ_Z?)?I z_23?0|GsIT6ZFnw#(mo1@MNseNzCFKFuHD#&A!w31Xs_7asT>tgT}No_>L?-+kPCQ zwMxZ#Eq;2HVj2{n5d}KzwRcBI!JWFHjWwKWYzP7fi|U?T8nU$yaOcX5h^w209!RDz z8pZXf>k{U?KJv4%6en)(wfl3zZ~KK{GS;AKSlZfa0me_TSH4} zeY<}gqA^)BCSMFA{@!GS_1;VPRf8&tE1J5HR17!4d0s+@05w`Zlnvf}hK zyDJr%hREmodGWnVd2yhHA=<)I$Sf>DRHXD?3YM5=D=7SXbW(>ym#J99G1W>h2>D@@ zDE0N};TR8o3ZjD(y11#FCoe`S_V`pay1>TIKwh~|^pyD|{mKquS%~8Gw8_pScS#-E=*$vPL zaP^rM^(`$HpS((HNvI4)-kSPZoPT0|rcX-)5E2Lz1N<`q6NIx zH&eRUialYxOS4419o8-#1{9*W$B5BmxNZ_6(~JK-Pel10E7?3Q6wm7*S>%#rJHT#h z+B)?Z0syugov1Wdc8-$%S?TzEKu%s~v`p-4y~hGTUn3{fwG)a~ZdAs-x#t-H z>3AjjQs$Y3S#=DJ0dS~nx`OA%nfiCip0?_hTjKWQJ=)3hzC%Y>;&yYF06J+4`%gvG z@D5K2k#@0tdqjH>0J$2RjGTsT&T2g!b&kxueGGU;Ad6iG@mK5Z?+twM7bCLx(ZCq1 z(PivDo3uaHjmo2HTjU-6(7|dL!;!}+zi~}Snb^znQeJs+CDMK1?4Y_XQK%5Yp<=mWbBytb5G@N3 zYM&#T9MW`ii!wnLn9)oh)IIW9(yTPH}_BTv7N zD9r2u2PdHa-LSAKnEiO|JsEjdv*`gmjqq7g53Dg&ofh~muPXV11J6F3PSMUzaaKkg z&QJ+$_DeS=Ud5|3Xk?{qL@?pKWg$Lxa?jJP;bFFgGg~f!Z{_vo$fh_&al-aoaxmFH zel5i{vtZ?FB4HF{dLng~65>X-c63n>qO!O~Mi8>+FTXbEGH_qljQ?qJz;kLVSla|c zyKyk`xBpCfsX(xtcs<|M zag~Cas`x7?=TR~==nNB18TzX5Hl^7uODMqvp%9alz1=B<-8F=(9?SU!-k&)0A2Dd? z=A4!1UoeC@5$2G+x<c{`7(6V*a-nAYu5MtQNZ@tn!3{34ua`~#bV z_)1BTpPA>c#u>xnT1oS1$G`Ow>PHtxc6jLr?;)F?5Tqmw z1yKx_(X@(Bnz89pX3If(Y0{)?>XZS0tGQRO71nf#A2rLSKXCuXvSGcUBuDvR|5VPGhL= z)R_j!mALubYs&b<{mz0yAvr_01fgF4cQo5*teX)`yb>OwN2J}KUNM&{&D zSzt*{ec^lVseJ4E+xz?b`}m$QXXJk4KGSo?{+j*R^B4%Cb4zSIP)K@@f;tgL6j(qev2t4hEHae7 z*%tMJu0+m&sibznVdkqC{eq-e0m)qB*?J6fNOnd)n-mTl9OlYjY+)+{?+yhHH0vBQ zU&0T4RvOgP?+y)gOvt@ADGV^tA;AG>%ZR1eA7JicjMJ6qBsD=Kl{(GJF+LvPg1l9( zahwrLZO8{fPj4B~raoeNav)gHIN&R;;wOLnR0L+Vupg~d1jqRE2rzCfL8JUe1X~ESVJI&|kWaw)-8=Ww zVh9lUw+XuM0DwAJ5NHMf@_FThgn)72Bgmm*e%@jrY)JUM`a8d8Vt44sV21It zg_pD>ue=^tLXN}owKLwQnY<%$$(UP{FU(Z*;E`;C{1#@*JJwcr?0;RA?t^I*16Yv4 z0APwU0r;U0M`h^%QTxAA^yUJJH+<*O0#ekjBeedq14{HE{Z*z1LWZJ1nBMSvf}e5c zcM9jpoyc?lP=q6U-D7eWR-2l40dYavB~Q!ZEgASm2T#RbRI?vFYF#ZVzfo-=M36;g@9sAzj5Xm+mNua@=K=petp*j%0hw4~+O?x$3&s4DO@ zH!i6oPf5N9nEaCt4vF_t5>_u+1MW++6()tG6-gFe;(6uJIDIAolH6ht!N8Yk1cBv% z9ieRo1>u8zMDTa$?&SR3OQ^$g0ZU_1cJ315;s$mBW};)^`XW}4B~3-vs%@3R4gb%VVL_H^>km zsVbOD||*?=kPLTD>lr&mrp8+UI6I8Zh0UilU~U)eZZYU?Vq8bJSJ?z z>FD@K$@;8O_{5Z}nh7JZ%LIVHS<_Pn5&C}T`+WacQ5F&N?>Iw(kq8U{)SLcDwUuB? z_fYi*ZFN0WC^&UN)xbyuWqVNN11RD0#H z@5YySI;15J!sjDs9UgC(Oi4F6F+h)3AZ^@DIC2>fRXQQcJj4*zn9|xAPFAtxU$dnp zb>gs;Q2cGDaLh<+qoUj-)q8P%gteg_u;jh5CotTSh^@z^R|iFA$;(Xty4n&C0=Q+M zU4ntUEU8X*-e7?bMV%OqIk$y~1Qrj)mzJIeDflOIu)9>6HhaF=0V)}DDw(x4JVn3i znw6q{9%DaP+wmFU&|zvyf|iM~R@Pw32t#y9Lzd$-^w0W$Cb>=#Z?;8#o)J}=)=PmZ z;f4L~m~%ka@h+g|WSXPD7aa^Go&$(S0cpjPfsmNSzP&o-Tq{#elW_`F@u0EHlLN8S z_+|9H3S4oi6Ho>%pWeV{swx{!1W9ccs4*HZe;h1ygp;1!%eCBtpn|V17#LZ=L0B!g zRUg}HxzNXf1l%Wf`i9Sv*7~fZx7Pz^@x!6{N028_1s==&KM7JjeKF_={- zHeD<>9a*7dfm~D>eUoMJ==h6&*cf38tF6BhE3RZ ztL$oixv8>~izM>U;eBjusF(e}X&EyaJS$XVH-^u_aJLLNoz)z?^ZGNM-;LJ2%5kI? zV+8vsDoMZd*#X}k!%25fxKfj^1{LK;asW?&B8x}*I)kK${z7Yw>X&d#*0 zH&v0c{NOn4IsNGGbpAdV-jlCRfAS=Oi$B)~Z90>2J!TD2kWbP^xxocjFQE=whI8Rg zDdftJw}C7hI71=?)7LSL zNF8jx_=8K1z#}gNZeL@6`zRu7i0IJ~^fw9juP+$P;^RXwAcajYWn_b%xq-bD|P15+;{pv}CtYX&_OT zjr(5RmvGE)QZ_8NAy+3r5I7BJSddx(0c9J-&q*O0%j1n?&Jp+2tFo zvL=;pa(a0CAub^O5JOS}kb16Ubt{k!%xuSbB`%bmBPDTMM~=I`*H<{vdMK1*{oT5g zg4W(#dzg70mj-&^XiaE^Qc*>SoaDUN_)&o0Vee*%!Q*S8q%WyLhXu!@JZp)50bqOj zybHp`kJ)J^Kh*ze9nwgS@4SL-KPL38@n}uvL&RW-mT1fTI)omyHS2Q=;CWhKo=**= z_PPVa4`~(m)=lAXE^{-)@Pxx8dx57n|0RC8Hy*FI@OnbpS%+7s$fp@iIe7zZ6~qTY18LC|toH}B zrAxT%XeQ**OcvfUA~9Wf{o{rKSd5eIdE73J<{ZG?uF$B1YnF{u3&et7e=rX>ugfVm z>~@Fax5j|)ujcu+MHuGF4A?u*a$?Y6wtlVfRBXid26(CQ;ym7XRR!qpdvOHDD*ERp zZp+h5RDL8eCyQz?&`TM8;Y`mq+Yt(11^!vTgtQ>upKS}CXFi9 zrv@BnVh5vY>V8r-aBGu>!lk`C>YUF=pR=s3w;sekbX(JUh3%t;q|Eu%!}?Nmui=rw z?29xE5}MB%8OEM{YortN6yLO7wv5mr@!~jtT_%e!n6Zi-`}t|mpg*48cn&@}->=lZsv=(>`bz*8EpxR$!IpNLN63+2u9mlS3)?Yn*~ z0~4dRpJi%yr4E$i1()69Vr=Pn_KL05JUC4fbho4U2D_3>sen{kHfs zUeFcq?bR;36YXDV)D1<@Kc*YOJcO87(J!H-s|wNET~eM^Oysu#rMsQwjE0pc8A0cB zAI{b9cU$|X>+Z^Edznz4j=E9w-Uz8h-3gg&nT)hzbOo4k8XfJYbbSSBs0$gsoC40+ z8r7&8@VJLb3;G&!aj*`Yqu&tYYO^$?M);vMk~q zE<(cciYl3%?>aR)D4f_DqxvK{7#+qE!dv1z8QR3E%%Ch-L5LbAFH&}ux!r2u_|K*( zCgPAj_UXi^zNhb$Q{#H{S16mBfVE|=%qr(s20T#wB&3Ec4#^ESh(U6v8m=<@;ydEu zq20ARymG623zJYpkbiRISLCZ8Prq9$GK)0cPn-xX4ysFAux7-?-hzrJR@^HL7%;6X zK>V8{d#^m)B$C(mh5!YaMe@vo*Je;$`P2l(m2tXX&s%^yZM?Jb3-4QkxkG-xH})Kr z<5n0If3jOw`(~hQj&Nom)1iNANu!bYGE`hNq62ouT<=bb^SOt-z4l?0ft~4%?;S@q z4SIZdhx2;cuags$?sEqh)DOf~&+j6CYg@pY_3;Qy7uln8dgUxSoRdtNyIp-yI_5LY zLvC-|`@0apbWf1~D01hg>&6`KK-%c-NF`a? z$|S>80pJHjaaDH!wUHb5P)J^LdU4TOuzU`}uf$}+1dg)1?5ZQo0PVu})cc}(zGsXr z_+mT)^qq-fVqshM5P1paO=DxP)^0N~R;E!=iz9K#>niL5QTHWe8{QJNn3QhIQCCd( z)0U~$PHA?fhqqW1d5!@i*gwnp<_xneVbMxu(vEs;L4Jn?vd1YjTyYnWe!A{oDEcmi z4VB6X8T&9tFGVqXvPbJ71vKKv8_M^NaOE}JT?Rcp2b~9&b>m=vj@lX;a}NFl*nulw zqgWq5tsSJtIMj)Ad0FIY)|;fjKTiEhF@0<>Jee@jS=57cm{Blm|5k8f%`&^iNGC00x9*6`b4&6qv>(-dHk|XD2giS)t_?=F)Q59J z)XghUeN1geo=H*+W}2{x_7D_vx6tZsY#-v&MV%|BU0onL(3p+AN+!@=;z4*u)Ks^D zLl~tA+_-xkrfxTSqCD3&P>dw>s3A*SopK3mK}EIjONK^E2#Rxv}EEPo7e;Ps6Qem zQgkxQad~44%rtg9D+clk>a+6|=gQX1EdOxg zp?)w?&jdXtB3@-12AEop5#|t?+J;+h!dSrU83v)=-H3nav(~5@Flj-TexaenOaay% zJED=MBQL#FCD1n*{XugqVXrXJFrJBe8(eAA*tYw$Oczh9hFlW(1lvF_uHr2>rNqR2^=W7I|T4Z=A zfhoPJ@|ibRBC%Lge?P@e*M)sFlg)xz0FZRz$t91$h6vmfk&HD z{({7iN8?s^@pEn>cGF-9?^(HNLL7=U?1T{ebic8DZlL-qDSWPu|L}FxXx(h|EPQuMdYDk2c7eL;Gy6j;!}P&L90CqP+uohG!0Qh4tyEeo zHDt<^J9}5JAgNpfMYwRv3ggjm5~6}r0dS}mTOAWWh0|lqeO4GLY7a<{rOBE5Q~$Xp zacu2|=2^m)DnWKS5VVk&!DWCKawp-oW#?_0%r#17?)L@WFbcrtwhmg^b2w+CS2nZN z_RQ1$#}ADYGb>>rr+#lq>KnWE!?kV<*6uaMN?~(|B+YlA*mkV6s5t?woI>}c@GD71 z6d+cXC{|Wzl6U^}SzilqxW+3Vh_9Rwafk3Ao( zs9JB*6K zd8u|c(64!}#&O{}GOiiu^~<=j_cWnLMQI0`7rx5wu+o0ufFLhzS;(9B-+kO~+j#Sr zJip=2!Ps1LAQrxyx*_boAERNUzt6vygj~o6%w{(LNCAd<3}*05{RMs}VeBwaXE0Qa)0cTU$txNm7FZr z{vlDM2D&rFmcRHctb^$kH)Ww7>M(kjmo1keSaYPQo|%4$nssIm0*|;AN)x_6^t3X* zMZSU@?BqhvjzhEOrzUL7>0BzSq8fJV+zJTqwcLBMc?I_+uxSbVr!pFc-H zI^3g~&8uvo3z1%UYkdthXbqV_6kg0xO5DQksdZvx98cDwn{2XR{kd^%x8NqOj@oE?m5>{#78S0LFEH^1rZ8Zh3IFVf6#X~kL>49?(6rS>YPZ+BcA8BYsXB-?(G?G zJY4BUNWa6;fViwMg_yNMpg2E!wP+%NIcGYEpdWZr&uB^h3?Cc<1?D#iP%XX*MjHYQ z2&jHhA3hlz^l#!se!wxoqD(&k3@9K(M1FJT63tO@olYK2=Uuf9Dw+m7nBN>9&_S+W zO8`urSVaYf0St5e>ksnN)@0FiK}21d1EwzE@AbHrxwPJI4_8t-dzD!GWaA)J45au? z=e&ut+V?vuGQxMC^eOjBdcr7Qh_?A8>Rm3r5P8vkcD*4$6Y3?oyMu3W7P6dLt@J63 zY^&ra{0{M@bH9<{j2+G@p*FLHTBk!5HSb?jDmT|0vDkU%T)HUqDpW5bvm@x12scE5 zy}hLEBPLQg@pJ&#Qeq;>U-zoa&|5sEn%s#4`1R+rJ&U=c-713>3+CUMy@BvVsF^glvipbx_` zthP$mGnbMo{sO)PnN;vMaanT@Fb@oU62wHMVUSLS5}en+Yx6IV#ofU2sD(uv${O@) zQdXEHu^9%I$2@!KPqTi?o5VnYSrh(hCxK`R?r#mF9ju;uqzfu!9VQ-3d_TKpHZv{S z@|YkrgI>1FVCtU(v##@;a5znC7-zMGBbQFNyJ ze0gnVRh{1tS)b{_P70Ia5>QdFCii&bE?$Z0pl@lsL3)T;XjV1C4kSFecCsL)dt=;e zZLw^dMI)1SFAFIw*J9%NvwABj#^cU?YA^|;j#Y#w9$Y`Vy6|?R34hv)fPi??jg<=| zhddk;2nO;76)ez8qz~}fM~|0?gdZA*l*~v^4+#Qfhl8RZh?ysfTsmdMOa8ry;_y z-6$@;DgJ&oeFM7t@UD^T`;T0N$VV?Id~qWUH=03p5Td2T7#eDEDxs~ zy1JTkSNC34+DMehocGY%oh027qfg=XiqT}-rXW2Q1 z4>3M42BF96B;=n92A5+DF!qa&FzslLxqO2q;{0rYQyTT%jdp3Pgo#0dMj&nwSqXs>Mq zZ;}JL$6)O^G!$vfo8m991S_%*mqh|P0_R$tVs4{WCTh>;q*BOYWP+4wAC2;C~WTG2#=Ib(%JQ~g?r1aJuc#PUmoPo(eGJ~#lNH3RA84e;JEy01_) zn0#Q-H}|Z<=j`o&c>1{T%K;dHOQMUwvn-0z0m6WTA3Q8l(8$o3fk(yAY3&DOkpY2{ zd28-GF!0Lu!qRnb@xVRZEQq!GM}`J=$NULRnE)ki&E|oE9F^#M{42i~uW%E{(?3TB z()&uiD+wsJvmS$qoD_tD4=olZDK;)Ko(|~pT~B%Eckx^u_I)-UQ|#01HH-W6%Fz3W z)7iyEyTYm0?FtO2*XNr8?1c}WiVxm*&eQZej<5>TJCb-w1XW;Imdm+E#62#sn6s!u zT?Wu)&%JCxRFvjQj!W~*cFy3=0UoSuX5`3blCvrM^G>+$*h+AEI0v>=0MAV{N5v#oyYB-tleZ2bf_19JS39-T zm-pLX!+6mjo}s==M3pg{rPyReDsfgq0&PN{xPXh8+gG@Go}0UZUpDHbI`+Q{Y`KM; z)}}{p;lqr`wpOOq(P?6m_#DB#Ha(*$ySo$nvoiw*LKI}pN;56n9~w1@!)Q1`tdQP$ zOlDP;){R2x&E`t;`-3z6U3vB=ca+81Ea^o=8}8D%Y&{jG?G)DjqhT zN%|l1rn`Fpkbsa8fZaHN>lq{fl%b1@{e5Cmk|XouOJWOI2tZB&h_E$x4*;|t3;zNq z-^Ka{cDBpUln4V};A2sU9FbpeJ9zRB+{JMHAqbuME*N;dXSqFEa>%_9biJp$PWjWH zR>1o-w>`GKxPEv{0HO5&QUK&IM<_CozoR&Zb~|6+3m^y#aAiY({7B`W17J_{)@ zUB)r4s#a&X))M0D;79R1Mds_k7?GYEW~2O}L_IS{2$AbsS$Xa&#A?h_cqD9?^g3@UzZn<5qxliF5n>^(kHVDMPm9uj8fZ!c0a<&$SN6=!2~M`^)zu z;oPFca-4~#TE*7OnwAK%p4Z_>r$sYv52#YlDQ@SOkm()S^+X<;bS$NbWuWH;x?+&_ zfw)OLHmUcO4C53t1Fp>clPO!4ot$E7v|^#I7!#wX5c(X;eYlm`JZz1&RoaNt0G~?T ze;>^ql7I#-B~fYkqzLece=Az4z(cV#V`M*258@KQ8QFt63u92!=a_9?%czj&08;mS zN2p^7p+bp0(0d5mVUF1(UfjhRkI!2uo@9wfMU>V{mK+6bs>`QaX0}6CZ-h>hlgb~C zdGD^Hwmjk9pxTj!oj3x|WEP%k7M$hT? z9(AkcD3YQHHdD^ik=OO4a5W0YnzdL3=KxySH1c^#k+Kvdi;|xoK|PNTZoM@Ax=PJ- zt0O5MWmXgQIlaEIGBl;wWq69z$hy&TvugH@x((7NK_+Vq#xpa?E7Bef6UZmF*?C)Ij#uB`mII_<7 z_GeX;{a}=4s_h`+B=*mWpzBse4qJ@s@2^eC`IS&3cf-XoUVG~KqcR=L{<^yZ>GCoX zHpEwKQx2uPVc1ly=;2%7%H~1LfyHdqYWJj*m@dAnCg^XC zFdqDEHsFWPTme#p_-RXuID`JeKT!V7C7*WiVmoiF( zvwfS61S8yXY@bgs55>xKeMCTcC~e@%=_MMdfvhN?ngv;#Fm^|}t=X?k^h$a27IHi@}Q_nW-V+mA9(oZUL;&Tl~}SI^dZ#obqiTx7BB7@Wyb3^|5;^`>?6GJt>fk zZcA5%c}pjoinUw&=kvw<3Nri+d$pwNgww?_^j(@4h=!_YMx zrefi&#G{-Ia{ZFIuL_UcQeO{XV$cux@MEyn56Nq}(9VPp-nf2psU-~ebf<2QPsCbzgEJthhJD!n?Pl)$g~t#TjXAt*%d4dh=7>x6)|q*^Dr>; z1TsKB=D2jJxStB(9Fr!@2&v+qlN=IXtC8$T{P|2u(n`%!%lDD1tvN8z9@*JkS1e&` zPPp#E-;{}`VA+XyCctX$!RwJ_(YX_mVvsoNzD4U74Xl^`j`Fc$DkonESAl_|;mf3T z`{p)RHPs_vVTK4!%MouHNML@YCx zx72MeT1#Mr8P4Q0O}RLRT{}#Zt)ma;Eni-y*mfWTcs-mRrGMj`g*%+Dv8Wx)GW1#G z+b0iv;E563kaSM9dFbm@@#F=rkGto)nR%Jp%$4c>Bh8^<@;%%$dgug5d3NsDZE+f5 zRe1x>QxZRTg)yX1=l0e3@cEfQ2_sArux#*N7Ccdmh*Nf%_FmO!oUxHmwePQyBPo)| z1E_!Pr;Qf}_m*2%Xd#`oR-)9!WA&nus&3;siNn&hReCdeL_;lYZB&vy$cNqIsV|v4 z;$1d=6^EVEWLZ85NM+B}N4EFbhqU4FvfVxeed9F} zbh(U5W^1bp1BeWpbnTQUzkPxP4x#ZWjt=AxVB|ahxV>}FoA&qSDWv6oaX~$j5efi) z=Xc#k&@uAmj*33(wszblpg~ z`mRuT*IxFE=*UjBX0GMz&LQiB5Ut-fOxw~2%@oiMUf0{z*~ey9ndW*9 zSqh~R*zi%kcvwzvh_ZtX}Ut1XhHM_izuf{bzvJC??YJIv6JS%=Sk@ z3LS;QX-B;JvkRpkD3}KG2SrKqwn%A+0s`8aLM$X#@Vn6fqxT&kk~)yaVG9dPu-K(- z@9kxL4GQZ#(u&%q;B3Y2VJQQ}7?*YwBZa*|#1~Gv^O7 zOPVR&IUCaxiBj@j;>}c&t}PC=QPJ{ZOB1 zQK)p73;m_Q?amlly`v)NpbE<5eO#!FT)~Bllihzk#BsFZor)(+B?g`SV2H%NJAwc#EKSkEVF%MR`QCi<{xTx2e9!~Bz+>tBL0n?;R8|-GR}{Y zE5Gfe@L9be?ze*+y`zY7$9wkhyxt1zwC(*H9HM+6H>gIa$2i5-vt6eZtg&skGYa*p z8$QE;c>lyCkY0RqGr!Vo_fTxYYpH}wbFPvGjEj<;{_B@>r6})w4kQwCS-kq^3v;Cj zrKKY-W;wswp}6;n)Y-Dyk}Ei%ux5uT^H?a2-N zX-8&BQTyeXN%r3h4UR~^Xw9eKLY(>}Bc<#X$6csh-LNL4c`@4`p-|f2yKcBh?&uHl z&*@pSYedUog+K&`4~BV_M7GyBw{3L>KR&FAT~Wz+7g~E2Zk(s2LC}z3^audKJ=;^h zBWF&&JH=hy64*NN^td}or5;mYMf^Tz90wuN16^YTJp=q^^TzcgKsbA$5SDlzPVk!f zD424VPT2Kv{+$c|FFY?dshxz7o5k&{cjEJty4%~sJT*^d)}<&!aST~S$~d+ z25+ol&)@dt8aovmyYNNSrN=1yS%!MPgS+?ro15LxECqC}R!mtnwcr&g+(?X4w}Tp{r+D7 zIv@}=0QJu)D*(WZa-62e{R^b+PyOmY1M6q*zh3%|j>b-ow2Jo5`VPiSOm0j}QnseH zG%B zXQl6CZ1jIx1~kQ^KKZ9I@t;BXUzS0I`CrRO{ghuR8afyo+x(lh|6953|6gxo7_$VF zpEUk|2El(32=dck;Xm~LQOMEk-$l~=kK(|zb-e$-RRRxD0$~{x~PC4vSL@Fxo&E)j20Z;F`lTQ&No$5kZB+Igp(&@FOsQ$6tUZgb-j^BGANw0{x8C zoSgjJ;>-%Y)QVI@B8F&(9d-aZ=m2y+1_JaSK|3u2T`O!qIA||8s@5mRkoSHA&qP4i z3fpxH+HnioD2@PsP9keX@;Rm}7=iW|q3V8g9-BzgaO`cY&uXi wpmGrgfSDf*V6*@*azJ1K3vi#ND>eb1VjWu5hX_xQOP2p zJ?A~=p7*_Xo%P@QefL|smiAQleyX0T9jbQsOlz!1L=1lY)OeQW4VNaKu{^?S010-rE?FmPAL zIyj#6_r#v@H$02+cf!cq@u-~OQu0#(3S6+>He7x#&aPexe#$&}y$awnbXuH;3*W@s zNtx$>6xzU;OWn;A%OxX*gkumA(p<9gViGb)S$SDeE=eGZBjm&-5paaOf{c^`LXzv} zA0A-M)6QPuw1(DCTR>9gaq#wbR}dHX_4O6=l@fFFyeKXqFE1~SkQA4cgo74vuPd(J zHhyqdFWz4oG_YP6Pe*reM>kh4NTZFd+huQM9>Da#1s8V%gTE0)yE7wIQe>V54B<=?FT- z##zG+bJ+#!>aC-p%mc27**V%N$lBP;N+Kj>;F5NdHgF_H+6FFfFKGvtkd&~Ml)%Wz zAmn6!>G@Z9a9KuPOF~i$A*m@Lp((8G$uVi}QQSsV3ajmrgo?pyg=t8BL;kgfu)flKM~?9_Gh%w6cFw690Ly%X7a zaN141q%5?dVMS)<#7u|KlE92t`-Md^t_shd^3NgG(@%alvk-+uD6wjDyii_4b~Ij4 zful7!UIxXPUboHWG1N+W;670{YhBph@kH-VcrjPrq}V%$l!Um*%!(T#%tx4IFBe*{ zNh*l7nraBQ^F3j0DOQ5BNcraQ&;Tla5g*=rQNg3&k^OLiU$w`${rwR2m zx)bzhL@6>zWXZ~i?U3UZjknv2p2&yFCysb{5q;8N34KLyo5++}>@b4ALde3VI)u0k>k%!Fab%{yj}4pO60HG=q)>Zl=q2qyES&!!RDY9vG5)6AkFFE*%ljt++^A5DkX6SKltElb+rVmw+< z3eeFs)F@H24l~vxBllnZPT>Zn7x3rT(fCi(mpo|oC+W-E#`7XbYmWX{xTLg|jkCAC zo2Lu>va7qZjW^crPqG^;(8r3;Zj-*s-+}NuyZz2?|6X>J(v+8#kdpaZhLV?-Qq@#L zBBW&yGE%Zy(tnlReqs^7vfIDJ{?2Z{3dg@?w_FN+UX#<38XH|rT>2p*S(saooXt@GL?f2sRAk;K zB3n_4(AZ*d{JSAJ9pUDMBLBJ_O9DZCXCL;TXl z#_;qB8=g1r$x^&bMD9kZ$)VyXwg2EBNwoCP>L&!sgKoI_6Rb^YNqxwV)orS@E9shh) zkkao^UG)2*20Q-|OeUw|+{f zYe{QKtE&H61J%@|Bs7p}5(r5tNdyw1@mK5TPpsNsVt=on|6%>aEXnr5U<3s}cL%qc zF1|8&6itopDo16@bJqRlk&HB2Rx)&GK4hNHmtps^s zW@Sx+CVF%&5h8)`an(L)&CsFCdlzsf!aEPQzf!~vlFf$6Th~hx+`-&&e5LaKom!p@ zsuV$&4$qg0w6cAVFiPkB#N#TloK92ckwLH_!JPw!*S==itTZtuG6W_P+mSAV`~-i^ zxGx(+^58V>M+}x)WuW}}(a;Y`vlL#fwf1k1_B2a>B7gF!>^Y;t*J9K;vxlYoKZ& zDM@<_(njvT_g2s)USuK+#wPG{u^+2Tz&c)T`@mKhQ!YvseKMMh=dl)BM=OCaL$d{` z1cfXL6=}yKseMx^&Q889^h^nkSPgZ_(5QUAq-3>x({lNff425^n_E_D%37a%c5Tqk z1_iYK$bI6x4IN?KOklx)!MIS?MBspfh0;JGAu$pSfM2{4o_Av-M3?=vy%_DT%6nwcF<2z)@{8u53q2*6eFhnU(bh)e`Z2pWliokiXX0ui9unhebL4W;?!=TeNc{WwQ)S z4lD<_X$>To6wpP>2y`7yqG>+~lraHiaz=JUk_>V7)OQSj#|F5l}S; z_~Ozukk$~w8z35;c-RqCfC?!XT#c830P!N_27(I^jza$N*Zc@m8o> z0=1%$jd~4ayEb6b0{W{ryPfn3G;XBLV_j`DDBKq_kq-}@c2ON#F07oar5i|smh6Xa|Zb?Y5JhxeF{>ut;cKejM!_rw<9hB5V@~U-fGK+u2$NLv69@E9<4U;#1C zYkYvfOVBMiUJax_Jz#kkVsnaz9S-%x3ZOhmofOXk^#moL=6f8<71&5FP|J0N}YGD+M}s)6mpE35FHy64S`{M4;>*$^iz(2}U38NVGEs_>a#`IMZn7AixGD z3pLZ%s5t>)6d?sj9&>Zx06sY(c$120fNFelN>6P!NdrFPlT(+zT_F@^C^^CG1uIg) zN&-6yc>v5{A)A6gGm3@BJPttIlYyUhJQZ0`Frj3RLh_7B04u0O=mKBG7wEzvXqcJC zIB!D4u0pNsdB!e7tyDoTCuX&8Bm-T>=>SrMvI~I1bQXdXqLnKkNGjxuu1UNz1kr?s z`KfjriNJjR;xLallsE=6MIc3>*O*q~DUn+6ou93l|p z$&tk*s4M5d(g*W%8^o*fhWxFSK0qiy2+K@UJ78UOyB!R96?Cxe(M-(U}tX3s7`n*seo(TVWswI8Xa%eB5jxh$*i& z-g83Wc}^_@K0L-ynl)}aiC0zt(k!gtVgx=6XQ4^ZKOlwIOap0NcS-uvz}B!6lD5~d zN(AeNR+g%-#jonezP}EsTRfi@)UrhYQWkJ{VF#2}PZynzz29#F%QMV46*4o#GF1Qm zs~gBxu;Wlz7LIV2xM%>Hx)4_dtym}aBtY#azv)peJ0h46Sgv70PBTwx7yyhCf^}y6 zgsDL=N&BBLXk*3Y+Ir9s?|pj4fe!p8tNRuG5RwOk^k-bB@apA227<|zU@dm`us`0d zRK@^5$)H|pt4eU1c)qxkw)gDA#Y4a}E!5u|pBaXN14gnB$|@KFD$lI5Ej?U7X#k@J zNgn1`p7={Wy9CH!JfqD|40~B=|$xz2a^VUwFD1Nu03m}_e zp$pJL)414NU7-O&6%J9epE5fJbTvW{gosD67#J(o&{7HV++ZY7Tow+vbZJ>Q>49Pq^dZAQ?MP~i@iO(zUA4xy4B_a?>(1C6c|P(tEm8fxVOm_SH@ z7Jo^+0nqlH9oS(_dV(YaMkjT^l~&%8*P7tUO(>=EG7QZ^7(FN>or-qy0wpvK3x-i6 zd2=h&1-giwFBgS7!IFM1w7)NAP6&n z6lhvZa{&4$)q&zm^h3j-(1e|a{D0dllK_H&!&}jrV!H@Bps^psJm-){JUM`*-vp6O z;VL;522AQh?&mhkfSe2<5dDZJS~P$@0>Z>a3J6foTz~{E3yeMB*&7e2feIig6nNte4UH+bjA-D87wDuS z0}EbWz~2F?*e}O8*$DvjYoNCb#1h&Kp_rgn+7=jopz5G%V;Djw0aa(AnpL4$#sq@k z3q4W~8W8;)Mi3WJZci!$y+Tm^JKikg2Mw!OFrm4`J!o7ZPi#TJ+jT6$+aL%z1YsD0 zb|x@#5F;*7SR_EL=%EYrLz7_30FyI-O4(+aXy^h|%BF+T`6J-i2(9;B1{UEU*TE(s z$Si+}sxly4mI-_ZCsW$Z_?0y|Hb4zOViY&Zk-Nob?_tXXCf>=h68Szyq)Lz6ZJ3;|=Xvrz&= z3&jY+n*uf?pb~~9LN`6IF-}nN3)6>s*Aw7Hfpz>o@5mGzJ;d=Iunx8yxYNK&I#iQ@ zLdp0RPz>!ECzU*C3czZDgRHnGJR$7|L#UAtK;#T9zy@^WU;}y-)KkU4Bm&x9fSn1> z5-4MZT-k|sLb(C^Pzu6<31EE)5+G-GfFKPCT0j^B!dXawwtAm|;0+1Tj%El5ERX>8 zOBxWMREghd2TZXcM1VI?@|gtNi!QLg#TgPKVe~^ds3Ye9!Wz_biO>j$2eB*y`-uuL zPx0IQb1_aRAD~JaEUjfc9yI8ypcPb26lqxyLAMQ1SVDG~2*VKW9B>TEI?%lXYp|oo z@0-d9J!mE~0E8Jp>_Eh8`=u)CJc418-{@#@UClDM&o%8EoJKug3$OYv2fmyqXm6Jt zhP5wb8cne^Dl6A_Y>0Q$qO^bw)4eqv3nIUJ&`^NZ#)|DWQa2DbQecW0(1yYeidh2u zeX2G-gu7nva53 zzaBN?e{LWlgYW}?|NI9B1frlg7A1%7r(X$5xC{=zpECb`%KYy=W!93Fk_O+&lm$mwe*Wm;U;W_VCl>ja*#G9I%>NsIaFAGF41VWBcJ0>> z4tfmORav7xeC?#saOC4RCdeg@zAffCnaJTu&X|HLz(t-Wlyjo^l$gWU?6kZ;8(`_E zAtpLq{^^a}x9kC*=Pt=9z19?b1bWFhehPFlv8WE=O+ITK#xg!2;rMkLlYs+$ne*Vj*Mh5-Q*K2QeWl%n!Hh*Uz^T!^IrNdU3fImg-7c|Ra>r%bF_ZN z9_?8Yn73BZ#WpVlcX#v*tjoFH<=3>iyi?)( zDzrL4VouOO=M*~Vl2maANpnNPfp`G z#8Vw`=u^`xJ05+G>U~)+V^OTlzzbQCD&j&lbTuvVIQYPWt~NvpgAau0>I9?^_|RQ` zDa*ysfx_e%Wb6fIZ$_(+v{mlq6O})cpDahy7U~L$3c{h*6`RB&z>hLK98ExFHh8*> zkfn-P1o{k}B*8ahED|l^$KA9h5p__dGC62rP4Tk<`oJuX9~f;6W2v$x!K);^nZplh z=?zQ8;kAUZ6#Z0yw*c^Xvm|gd1wZlfC1gcFq3oOnkaAsZvYUy-wPZihFIY7scPU7OBD~^Y%zJ2 zHDDt`h$k6~`s+(K@u2&7j4&R8NBpO!@9UVG6YhnXD8kbBmR&4^U+6ZzoqA(dx4Xj# zSl&z+{21zmOSOFc!)D&d;a&I2V;g?!ztpdwVCKtY@tGE&6W;mU9bzoIH^CwP{PCED zp?mQwh}x?aM#0-H(k>rI+I!kgCX z0!2B1CZ&i21v>Txr5WMP+_JEnxcfr?#nU@7cxV|O%Tj~~lN937O~?OBmpr4Ij!QZa zfYE&u#}7FacFwUVheWiA(u~8WkrCnnMqDr8N&?>@ItT(7MrP0@61X)5JDDsM~uSH`v>> zYs2rrtm;Ibtq9tvInXq%MyBSRqb}z5?gnisb7hFT51WLUoT`c}d-_86c-$bpv=LgT zKa?SwsZ){#Fh|fQm z8|)dRx-<4tu0CNpye-+g!Y5#9Pp&(3y{0?unM1M?f69MIVfY;PmO-&qQ77g|KzKqzl(bCE9Kw6BL4s7SH!=I`v3V~5&tgge;4(?i+WJ- z|1Ro(7xll3`rk#pZrJ}DMLpj?7xjOC9ngPL(*J%j(C?D|_ltqt#h{lH{cpTa|NF&2 zzh4aW|Imwp=>DF_95HULfAYEtNzh6X|7@8*O0jq5c5M^~u+ptEWVzWl2)&HI$Ff6wwPlp^~tZsZ^{~q&W7S zd(HS8|A_s?$5;BKM~+ffWSq63d-UiQUykUj3Hmc<{JXz3DZYK^+jV%BVfEqSj1l5- z`k7C81J6Ycr|WgmyPEFKOD3z0zcX;pjT-pju>c*oXY>2@`hBbg^9J2ZEhF zZpFlX+Rj~91?sdmYLBt zFs`^Vj9P7WD0#Hd9<_#AG=_^u?GRBCgDvYx9DNgthggx6kq0f2$l4T1Ktj$!Eu>0} zK{4=<6A?Fi5l6_9l1C5k#PCrIY3^PXZmoHCl`+DJCWnJcxH20)N!P_bYpEj}&}d=9 zg5mbz+&Stcn|h~?uH*VOx)>3aeJefFASwjxs_McIqLINEu=@VP(+J7vj~<0T-(m6V z_~%a}{;+X5?`VgWlKA`AivE+03nBFF`-3kS1@kZW|4stG-)Q=8ztL14A*CiQp#iaR1sUnuSZ}zYbg(<{edd-`Cn(K3FHU61A?X zY&dNF#KG&W)*yPvIf!c*RTYD5W`mC0gMYMP0Avu4T$*x$pGQ)1B)vi5^ z4to=cCL(5!M!pgFBa3L{PwNI%F~A_i2Cx8>@hncVxje`j>y^jRF~egdzs}n*bakW~Y}MeV?E- z3SGgiz1eVR2Cjgf11%n1zMUo_cGe|imf|2IAmC*UpP z8V^hZdU(==+IRw}$pZ(;kK!%ELi6x}Cr)k8kZ`W!cWWBpsr|+ zuVKe-v_Nr~(i$sk2rcgj`k*lZV^9T#2$$+#+F@+IJqCkX{uGc&2h<>eqkD44;~NGKHQV5vJe0)Lxtt#kifIkYxGM*bBvJ^Cp#4vk5EQ)+EQ16R@;2d5Cir4xE@_X&oPz?%1`Q;xK~z=ffl$yzcMNs( zBKA(omg#K%WklI0Y>8|z+aY51m9(XEoTbGCxNCuYl>eNsfU3VGN{}~ELaqw99~gTQ z*kUXb_{Hm_z024rA^4S?;os9Oi10sa0ki0jS|FuRtDhKddhwS@LOn!m> zTnFg+f9CNA9k`Bd-bNVDt);7K!}SLLyt^qKCsvD~hR(uZzmC6lH{}1}e`#OsEPHSw{NBX?~U z&eSdwjH*+rG4QNaswt@Gblccg(hDqM^U5f!8O%#R{QPx2s`%-nLFa6#9*b#VUn5-u zgBo&<()$M8b)Ro(>d9=@?spb0a|MnJ&_o=ztV?k371M?MXe^aX;RSn}#N?jW|Q*Lfe84;@HlLX@TSQ=A(j|*pq#g zmrzGs7+)#tXUj=kh!uN)F?rd$B`Cb)k1e0*Nk1fFd`OA>t?>!+09UO$iH9%WyZ-v3 zeUW!^>G=I8tUGNQqU2iw1obk=Z03EVsS)(3`-(r`Tj4P!E~_0r@gB6o%_PH|YpenG z^ugzpCW=ngzFw4TT&7MgZEYx8T)0(n##nnq!E*U6jLWX(@nNa(8Z{+kug7+A@Sjgo%_*>hFQ^-(}rZrO8TrZ26d!oDiP}hq8lA& zSIm--3xv+%6-H_jCs3NsEfHS@txr(zQ7a3|2+&kZ9IM@FjDP7E|ge^YPW3CAfk#*{Q*V@x7D2(?fw24~T%qmc?&)|G{zU}Zw zS}M~ik=0Ds+N}hKJ@Y4)D(yzSDQn&uK2@1G?>IMrS@M}zwCGnxgL=)U z1&m#rCft7TbmQyTObKtI$ZnJyuYE@N-Pb}(Y;ls#ZB`N!U*_hMkVIE4a9iI5oThu^ zw4L5;p|Pq7gPgM6&HfidH(QBU-fL0BN=%5Cv$xsKrXjXP1p;ppThiQo-57CH{|Tk2 z%Q9k7QaK@CTno3ko-ujEy(nlJwoI9_U~xn#W}C(|E6S2%RzyRbYJC%VQ#oSyIKr;& z)TVa-KvX-H@~!0yTavkBYRjh)Np)>DLd;kj)@Q`4Boa34l63@6{SupW`&%E#-JwG0 zov=q@M2z&`bS}M|`(l{z3!+yUPiQ1voZS?Jgy)H3o;5n|#ay~xRI@}PaCa^3 zu3EQITA$eCecQvtdL`o<@5b22I$dKPR~$0C(&EwEDb+xG6?L2R-ecC_1_tA_cNO&K zeQYY;O=;xnYmlQQU~<0k8gk~7qFk~tByWw2?IQE6gN&%=o#Od3)|m84<&+Z(Toqli zod!efrd+}g>NZ(g&mcKr=RTPub?!xL3)`Pxg)_Y_Yp4y_9XwB0Gm}=hwGl&g*{t>o ze;S{8=`og#s4DwNy)m$<*(b>`DvP zoCrBf7~?Wr#`&y*UnNguLjPEb8$;dPJmkaO~}T)8(SKLjQlOk+#Fz74YI)_?0O^6hc;RW&)GKCAsx zXIqPUbjB~dx!SgvcamK*VPeRra5<1PNRr`V(y`8sEdSy6@;hEO?;T$hg^u3H*_eDH zUPYlE&LGg`lKm#4Go?K+bHn(yn^Az4%jRKoT2k%c@ZuR5bTX+Z!?iQ+GdisAS10?bEG=815gNnb6=*(_aq zZig|^$6aMbAvz`v$peQ^{4fZmtdo9uy4SYvaFxZa)5C;?*FVLDix{|?M&y-^CQERl zGrjgj3$r^eE%!6Iri(u=Dzhv6A)RH5WoFJlVU6$LiQF{1D5sp3Sfx?l zD~e2?D)q4nao?5W@H38!bec%C^wx_0Bdl3-z@vB_c6oacpb6Z+1;fyZBT zKi86QU1ZU9eoY~d((np>re~t!iJDgByf(wq!c$0=Z}`0F11_hpgrm{J@04lhOl>Rk znWw$H;>RnEwC=95U7XRn*IjoKe#@D`oYK#G>kh=+FrOYQWT zqjQy~+0NSqN+GHfPM7vZS%!8J(uKdM&@K5MOs1lAh;xT*?bO-b_w{preQi%^q}Vyb zOX&2g-q=?6O@?k0^=YwdghQ#C?&=p(Zm)m<3F^?$3LceiQYv17X@ZXvI}T-% zc3kC&MK5@dWWTeg%s}R3bU55H-8r;ktbcbcV4F4blF(HLl$n&@M{&*4Y@UrY(nE-= z`_*d7nk*R#+)p_9rmoo;@6uo5Y`?@^@$FHOV~?uA=;;*2^Sz2;&+?TS$wNpPW+V$a z3PnasQj33k>52l&A|TpvP$TKhgJ1yjtYGy99^&MkGVWenx(#P zmGBA1AI59YX))y|nZ4$FRX+cl^Z;uh(^Xb7&*??-CCW>&Y>`ZYa-Y@X!rM_2#Mu$G z@`{{kVTY|lQ2X^>IVmJ+ER9TIo6RYA==I69(!^%33ar_jKetad*eGpzG*F0UC!otX z_TKef;qLO*h=dm{r%Y-?#^sCec3mvl9Xfp1JncGI%{2<{x zQg!;#GWE&a&YO9Y#x%u#q#t#^iSH+l`gZSxR~ZFx5kC%=(g>n5@3pAOO?A2Y%)#1c zDmeL$gCp01Ryqfb2+sCd-X8g&r+e^In_z`WGNUw#@3%WzqtayjM&5Wut)Kq7OH#At zH`(S(zmYxdzb*e1d!J;2wovL50k_q=S7|{8y>24>T_d$p?zMF?My2}{H49WGs(Jf2 zVK+93#x)BLhnm(7yv0e?VY9H}GvW>l)$PS;>OWj>n7cim=R$ZfkD~a(Rnre0 zq+Rb{xS#0_6Lm1BUDF-Txa}^bl8l>q9mDt_O>4me&7!uT(bP`yp_1FVZGeNZD7`Z3 z>z5$0B3m)bmX{;+cho;<=3v?QeNhdH8b0Ri^=OkHe1+m&ccS~EY)Q%C zN2jGW?Ny*Uc8>r2UXFQRe(Cz<>5mn~IR%^z{z56G@874t;VoBNA52)9(CHdnyL>YA zVbwtX(&FW}Hx<__*i|up(KdN|4JW%ooAc-o4J3y;*-N>qRjx~)G~6=vOPrLUxjgH5 z(s&|yAPAwBf_f>PZ$WAvKZz;0o4*nBO+Qe@D((7C5BrKG;kTxyqx#$IcfL92ktc;K zZscsxEVs-$@g9%ZPR^OXE@RU(r_r-WZW7I*>R6^d-d=qr(Zmy@$f3$%!LjUXgM@x#HQt-3^xXZyO!2cT(>cC_TXJT6xHxrVOAT zmuSje6dCtRUp4>S?NbotoKJ(XM+HR5+6>jAbLwN%UHGJPa^`PD3_NHz=n-r$meuv_ z6FhA#c*2uqD)MDq=6&69hm@&=jw`1M>w`@m?2Foe?iYOHHEv@2{B6zLm}`!;{Ke5k zfvC~*OVo>LZNBYymsn{Bue4AVtgpJraSdM2A&{5~UBRKH2-6mS2nkWm7Fw}9vKkp^ z=vY{qtX`g4_`&5G5&3H2T2p-ga=@3I4AC(GrOU_9K0it?6~69vY;Eq3GLfK7O@$9na75m5ut&YEDOL z7e5<2V4q8}=r=Di%c=Vm8{E4>XUw zHoDoy`ZCLHQ}&kckBqq4%<`>U`$t`*8=fU?qDna8FA^TqZ<3G8KlgR?$W!IGS%e#Dq$#T>c{I&rZtZH_mLqDT>US!Q3p&AzkKu}p2@ zqL$|JOx!GZV8ze7c3vbXOGoT83{$Z~+Fx3aF*gi|P-j2k_qb)H@EP63fCXEP3z5k? z4pxO9L)Y}KY-=j*)ed_Xi62`C@7`a3*L(evZg?^FZ1`w-e8Ljh(vPb3;~cF~K6w#k z;*|G8YBS$!;~K7{-w8ebTzx*kmvcjiH_~b;e|moI$J2M-cQQ4cR?>Q4GCAW-;*Jj^ zM2cBnj%-f*-E%ps=v{Yr`ot{?L9VAQIVIEb#q{+wr=L-~=H%$-WL^$z7{(Sb-bDs( z@rb*$rRGrS`ONcQpuS+>Vxi}Rsx#%)MTrhY8zT-Y9RGgfaY9wK-0_?}vO%U4AG#XO z-hT1^tKz3n7qrH&J$iSqlqu(kYIuSZfk_VO{E3oM(YvLbHzKJyi+O`Cc!)2&^eN5H zo8P>o(AnUdlVDFsBXFi>Tv9h|Zfj_oyEP|_gFnIwzQIn`vnxD~D0E=$WWCL$NvbyZ zY&f)6E5FB`+HKfi)0+4A@*B3A?;P)M=QFcp!1ZnrD4I4F)Lqg37LDzAa-Ht-^vyeJ zU*M6o0k-E5UpP*OGM~#ic5=f;-YeQC2km;?`iI=T4@-#tyRK>GefI<0Jzz%Tgy$y2 z9?x0L1rQAHttvR2`&zP$U7;o%-F>8;H;#IAVPD{pBmcyw0vfUw0j=&fc7`fTchsxC z8xu3$B9}Tl4`mkB-R2V!+)zlWRB1}&c-Pppki*N+o5Ez!;^meIFZfXdKdP~B;6{DC z;mmUd)hwI)-DSCR&pxL`=fj)x<<;je)^Xnw?XxzvZ;>vR9)xn(cbSvqzI&$UbMdPmM7|=_r6|$KW}7Qe*EC6 z^cM0jNs@L7$}DRdti9fgx85v^vQ*5&gM?QW%Nx5oU$8#rKeNt)GH=w-E`%p}XDz6wS9q#HvPef|)rx(4S1>S#_Cm&M!tIVPL6*VN+P0tJ`SH&7 zA3nwjr=Qi2=c&Gr=6TJsK2cv2Tu6ktI*eIJO?> zx{ao-scp6LCzRD*dEhp={qjFyc_Y_NU`-=__A74JwsChM{*OrT`A|1 z64Uw!>Cv#|<)i5a>`Q@Ff|uD^KVaZ~443Rq8C98Slc~@YUY0Af81X{4XnoDTUO1=Q zN2NPza$!&81Wz`%VtdLd9zNlYOm}6r#n4+}V+B__rQznVw#8!K>83hXs~ckh z%L2Taqp>qr7`_=eJpMfQjMgf|DT|R78>Q|y>eKx$&?x!pMc6{U!4c#A)u@-IMIRE6 z(#qc*_Q*|_$wxocGZw4^MM)MZzo?>-;$S&uyJwD>TdC1LSDnTYWTS!U!{6@ zJ88nR`a^+n`TN^q##67=<8lWOpQSkvVYilZ8B$`EJWm}@oRbUeOFkC(k=a=~@`V2t z2dcbzy6rdCb$W`nEk03%OmiL_N*Q{$)RxU13^!@0tafvkmKC2bi<-qu`hPZLy|QtB zo4I33v!aT}RNF=YGZU^|dH0LqmthhYPa#B@zRrqLV(Id|dZw3jZnZ_Ywqp-v^F&<~ zX)m73{mxrDQ6JHE;)|}Q-$%#(S?c&zg1{oVwBo?6QjRXi2X!r<8ty)2(hd1^e+5bK z^q#NU(K{cWIK?+8ghqvx_-(O=Iu_Kqj3K%vp~g? zlx2ULMpBD=*+lbAAH?Yb>xHJ{68HZOm~h;w|sy1(fzE_t!LoBb@VuAFF{>0TO+*IB&tiv4|hzt^7QWtwsckJwRKHpKOU>T}JG zuWH}E6)?=&vZusa>XGJNyYco&r5gRR?#lB!46aTmk33d8<|mZdA0vyNKld#4ZuR4m z8gplxZunY_1dl6!=g|9>?AH5t*CsB%dF84oJ=RUw#zVyZj+(2*Fd($sr<|@n>b%$l zWzp~!T`X<;EZZaMOyjcPzUS@R=T;SwFAdo5X3rhbh?LLxpp3mu9N)v8#v5zY^S-N_ zdnhWrYty3tqg2+L9uD-87rBp41?;ythhEyccgS4Z>qmgxDRWLm2I~ju3?pB9B}EG# z(r7nFE8M$XbS$BFTC8yXh@(vLJ`a1S8;1L85E}s-`F)j@UD%FH&R)4WMyAtNcJ}6$ z(+07Ftb^;Lo?f!?+m>!f_Fx4w`7_tzTZUQTNLmbU6>H^tIEer$yk|N$g*yFIKcA07 zQ;*^mMzlC)rS((u>x?`XsVu8$DYiSwf)^$q3?aHn)YayIZN^wb_g%b`4%dH#`kS>>CB{zxy7(|9JgcUJ`9)a5>GgTl&t&Pn!*4 zLoT0XzwO*tdDXt5D^ByYA@21=XR}8|Jf|RCt(8Q?u5I19_E$u*FEbjc&XEwcxO+PJ zIdUPdm7RPxTbYNZ)0lCRQ9=`uP~QrYCj7W>#$h-eMeAL~^q$ZzB4#8^?pX9?btRJP z-%z%Rg)y|}oMk<#O1Ebd?lnbv&a#}&j0uKmUK{K0=}fe7x0lQI z%p1S8Z*XLAM^`9lmxX#!ZJRMCQQXFW}{`ufz9 zy^gl&*7x_vCPK!r9v&|)#f?{;oX|e!t2S`gd$TRjiV7}btf)TU!b$a&xu9XICx6x- zk@sXcRsXfjlPlsC=uNk)CxtaE_u5P9d)KPFz56W2xf@VJ(#M_8w8>GKuN$6OrRJ)c zz3QBKSoNjI;+boUhWrnC8u(x5*FSPq?(OqNy!7t-+P+Tmbik)piHYpdygiZevqjoZ zwk9nrVs&{1H{oF=p}lH{3vB{PRh>`CD<1ZkKXj7J!m@pqyTn^kacV)y2Wh)}=w>`R zqj`RvcGj91?$btjbgz3-(+HD>86q{Ly?;F*vZny!alBjdo9KgUq6;GXC zd!~&GAa>(1xR>kn(xmK=@Y-qJSpV`Ay~EyXqJcx&`4ih8vb55V1s>UFQK?U^3Si#t z>l+wi!EvX*H`&clh>+BDNVe#r7fRt3rd9TP~_B~pQ=Mh3hsOy zbW+=bUsUYBetZ#r1A94Y+-!GKZM}4>xY9<%{(V=rn1~3|oNtG0=OvGsb!098- zDUKhHKGQP(?#R9rc|89`U}j8;?LK(T9 zXb>dG8P_pCx=gk*sIMD-DWpjKD&^!QP*F|avxvkZ^Dg6Qds}cor@vgC^fKi z&V3j7LB#uAvrPxd^XW<7Cd!Exm}NkIfbb=o3qpJA4Sn#+)T{j;u7dsR&M|mse8usm zM}jH)hxmG`_WUFr_%6)2SF!9$~5-Urp7isqUURed<)r>GSl{Gt+7C@7}$~N?&BQBMK|x zi)$)`jMoQjJ%w!s$!4N*h)GX)pZP`^VE(J5X?#GRoJ$oXbSiISkK z)9M8Z3XCuNH?=PG&}`M&Yr%qZ@8>5Mqv@k${laQPyX1{qT@mU|8!`dUCKA_^uyIZc zIqLbbZB|BHXQ*Gn(?pNh8k6Hf2EQd>tjHz<=a|TAF`i$}`RAvfbqX<)`J(D&yzSpC z#}yZ&PuY09!*ZifnO+3cBAOf5M9Mmqkgaw6&&CJXl<=&|;1-Wt(d5r(AaWmr`3n}G z1$$^Oiaw@APcQN0FJwucY&l^V!mGAF{7$gW(7(bzlQW*EwxDdOKaphJg3vL8=hS?B zlY@9hh1dl`=9M;D+t0#*dOlL`nLRwnsv9-y%-R*9(4c%Ec|Mc;;`%%bXv6SSWjeM{nECAffq{;UPRmf7_G3>Fi?N0_8)_=&jM!Vb zsLmPcN7(E66%51K(3oTvSO#&!JXx{` z!g$fqQw*c0kZ&7KK49tf6%lpSUpSqm8GjbKRrFd@do z?6;f}mL!uE=A-YFyxU^=vP_`TPi<&u&4SP!%yeZxj8vYuEhrrFdkXla)w4}>^Guei z8@xTm9sW#~JJFogL*&^l_LX^1O#vjlTaEZKAWI(pELj}0)-(MDY)fC-nALS#ianpx@xDUs$y~3Ipub}T? z%kcn)SGp0v{qu=ye0S=HIcT1h3~l-AOT6|E;1zc8qKFWG?`Ctb$O{(wM8F0{ zbb?x8#${CNi};`UO9G2w)cav{gZdNcZMK2Y;>DWCrO(NSAf=deoUsBJu9HMGJfGOkW}^(~Th4=eQ{pj&1&3WXv}6T~#_qGNrv=VC+WN8bXO4%T89pTGGL|QcwIO54?t9Z{ZYRM`t*gn6TaAnk*uL{<(y7eTdlnS z%IL^J3!xb{niJ{|ccLc`L5ng-L8HBhJ~7S@T2I7N-nOB=f5%j)u=5Pz^|!z=QXz4H zGP1R!#Ph^s&{|i!P#;HMaDAVx{alA*%i@{6o}0s6U88W^l#y+k-Bcx2j?Hv=I?H?L zVc|d{v1*N?1a|M7#x;OGO?x~y`pn`?2aCGuPwt;(p>7qLaSjc)YZB-fwbHU)5^=At zd;yM~`m1dSiIJ7zj)I%PE4Ucsw0s!E5J@kDfE15`20A|*h@z14 zq#w(Sm5&RHX~$4G+ZG!~8i_VL@z?k6UM$?lbs*(`;Z~AxkD%aLBUHc`jigC3kU58k z`F1F4m6H$>3`folBYNQsbRp*^D)7hNQnAb!oACD>HBjw@%MYO1h)Sf1of_s-#@SHa z5GZG@c2prbAcZLZ@FxBh`7{}^4gLo}@kt{zQB!HR)Zw`LdJvIw2k|&WV}O0&sHM)U zS*mi&hFAxoTUf5KLL z!cOCa7dpQ8?w~J&JjK!*nE6&1UnHx4<<;KyMH zb;$kekTl~E&-k3~LQ}0*6pkHNYZ05{_vctQFNL7Zwy$2=ymoO(wwO-Yc&rYcec_gz z4fdcFBS#{1Si&RV4*9_~S<=L+NUZryWjO?9f}cCILSN!J6jr}|m+bF-(TLAa{V*1m z$jxR+bJ}SJrC&`EbGZ;RltsA@m;w}Cc8(%`$2_2fe0jT(x6NmTcz`u3_ntUs2e4NL zH!w8sWas5_T>jWu;-{;1?v9q}m-CJWb48g$64AUuZW!TwLa5DZIt#VIbOZTF#vdxw zX!TkJ(}J||r1R0m{$9Whh$k{lvt7TYuPo~L+`Ftd`w#W9KOF)hw|g=j-Na}B8<5zpy7^x&}%CLt`fvup7(mK5?+QWra?%(;<%f$_@32{H6G{Oex5%{5`9TeBnO-w-bAa2AgQk9Yy< zWWvZFaS4;OxPd?MA<3lH6~e;^Ql#fNRJ8(jZ?d|(>UN*zii%*;W70Qg15FNA;aP=V zxu#G%t9E4rHc+O^V`STR24e3xEqP{d@1tU#7#Un;UA<=DV*%%S$pz3jxtm74^K}1^ zShr!XE>BoL&6G{-F$gJZ4MJlQS89<#r~au+WVz}2yWd6oVm?^hW+OWx`oTN|R({d94Hin=E&BWi~O(yjYm`{Et};KpOklh(sb) zy@GiEN(l{0R{9ae8%$V~uS*OjE5L=`cKe+h?HzYxO9da%U4nj*>v&H*7t2Y zLGg+5n&pYSDWE|X-;(+|4b>2#7)M=`@Zf}{i7F|aGsD=JsEMPI_w)N)-NZ-Od?ycqlBmF>&kr zw2eU#5UfO=vitu>xDZR#YTrqjw!DyJ*#Y2oym%W06#PoCrNe!p%eQ-~T&mOZ& zo)bRXv1=I|~cNrN08KLg-}Km7j{fc&5F(TAsjcklqMgtXQ5J(?%VjTY6f z3WB~>Dk-Zo>@a?WU8o+lY|`3g5-SdAt3~y&N(qmJP1pgvp3d#j+xeX?Kf4DV)PW`? zilo`m+rphX5`1r4vNv7Hhfd@H=-$e!gBL&#|H`O+nz*5yi^B#)6<(hwXm%p3v$NlX z?|4t4@Cy8kf}IIWGx!&-HfP|xY<$YCQ`leO7K|Ij9S4#QC_CMxfwEfq9Vk1vIZzkekF zmG3r)TA&vQ0I3S`_0PYR)}19M=?T~u7dy*ucd5YbQM4tvhJ%04NPJJ~vqOKvbb#Z0 z3~JBc(%jDd1^cs!;0~ne$T!Qc3)&7wn>%p6$k%FTt(M_fNUL|%?~8cnxR=5ICe4uq zVJg}SM9@BKfKtefX(v!SOoKQB>ihBXnUQzoilwX&0xRdqpJ-cXP)z1?>05A8{=@Ze zhGOslPj6u-$Zl$mmK{Q$Nl!PZY2XcEkNa@|{l9JuJGvG^7(tj94ZBU0E6@o>5dK^~ zU$)-MAUn<--kwHLH~t;ho=cHeP`rvBynFC2gpB}{Coom3e^`B&zD!f#IwS-);5$pC zL_OL5o)CxG6t(BQKfb4U?_m&W(=p`U!7qXC3F%4AWmtV>#_Ij6@76S65-{#xgIztFT91LqkNR=9U6P;5Z>>OhQpnRnEBM^U!mUr<^U7uLmds=FYqVA(;(BDpo7p9$lDEc;mo+ywXvwqUq*z&x~~ zy%)d?p#>7rC#_5$_x}vM(e07bL2?jw`}~p9k9NWaW11{gfxTe?!$lw9;-?kx7ZixL zv4-`kQ^CJOc)ItTG-nh1vH5{Cort|f2$(1M{_5-ku=sd_^$fMBZYsgHiS5?0H+LYr zO;`RNJ$g~v?Wso(!&?1l`L!eErv~TM-7_6_$1C>X?e`3y!56)c4j~}3GYZCp`NGm` zTG>+$@dVrPYln)XRgC++1rXB}4FNCSJqk9B;uhCK1<$9rBTD7)k=w%yd4=)v+m8tI zMb=LZwgN_*_J#tCh);NdX-@+z2JNT9PB>uxCmvj!+}>k0O^wPT7^ene=mf;_4FrVu_NJ5aGcF`?U}H*kKPV0xrCQpzTB21nTTO>D_h!GyN5R7O))plUP(jjHhUr>9q!(QYXUI4Ha+ybOA$U}WqMotwB z{37Q=x3NV=mpG_v`8nwOwLGdkJR|awI9ok;91`owq&|JRuI#eKzjL96vr7t{>@s0tZ>u)+Q=AHw4ki1Pf2m= zy`TX1Ev6Qoq?3N9fBHG`hV(_Hh$TQlIwu!yWQbDutd2EX(Z4^UODuSsravZ@LEjYd zSN!h+$O|h5$i$-(%?roZ6FP}|(@BP|f|49&;04X9i$!@>;=~=z)1dTGTPI<5A407? zw&l7Lg^%ms-{LA92*UqnCV67>5?VTREoj@p)-P4Te449=3YFWubIPN>(=`f>!8MRb@?3Lr~0RrGCMFn3e{F zG#TJ9O8sE5c46t}$)dXpH2{t@ku&iqkK58W-BmrAqQdW`dgo1B*PUu=NoV+>?`g zCPwmVqQ@7!p#7cb!Dp`1jeb!Jar!|wOk4w>UJ1HgdNogdCyHfD8 zG%b?)1O=a!6QIPxpI$khK+k4%Fy-HVtijdwtn9rR5NK8$qzS34Dpskx)PnLBmSM8i zI>S;pb5s@DF#9B~+;N7!cP^@g6DQ!EjeUrSg6=e~9J(gYpG++0%Iw7FhbHLCPxUnY zcsY$Hm);Pd^5I9qbx-!GDj&O^LYAZBl+_E(cAn6xYKh>3*XNrob9!AF4AZO`L6>|2P#x{A9f;RWNnY6@V*FkhShWkuGYDaO~es(&w}TKS0R@y4Dye_4Lh zgI~cApbBxr>omdnkGI6mNo2)hzZk*Vhn*JN(3J`!Fl3AmIuafyVG8?DO?$@J78JV) z^1@&fB){KAJgxsw?Sw4O%saOxS%7K8rr^dZ8*gaCGer(2K)EI|OeAvwVMsTgxtLk| z!kVG`rH?qW>Z3LJfVaU^=szG5AjU7zK7eN3&ktUG3C#;v^S9ycE39p&KsWp^E>X1p z>+w;9By!&XU&LQY>fk<8#Nao~J*&RA^?0m12ol>HwxNe?aX!zRAWj>P)sT^1%hDOP z%>%Djs9wlmvW8B7*7PAV3ilt?LHAu_u)qkBR^YL_x!Q2;Xm_BUwWB$ED0}#QZ`Xms zJ;OQJuoVX6W0^C00%42#yimFOdTRfmbMw6lMQ=)pgqB%W>}oiKh9RfO0uu6VACX0f zpy1XLdsg%xc2FJyL}#MjdQWjgXX=AC@%!;at|h(SR>FJ8B-8Hi6MfeoHSiP8EtP`ZYJ|5+oYG@`tmwKoC?d$P}&Y&-8GZHQP3yRy&Sw0lFEBATazHSJ<* ztX?tv(Z*rgQ4>DSLpwcs34ZH=UnxQueokaBVt_)@cB-fnmeY6i2WKjX{6LLvt2QvP$QOi}Z`#ePc*&_GSPARcv zOhsKeB1@xY|3c>NRG7Hb)Tm1kQ^HE$Rnc%1XL8U$eU{;G*O?XzSH)t!E^eQX!ZjM$ zNQb~X8l<0%EC6c9T$Fh06P<}`UC5rOTUz5hyoq16hWgHI{<_2%)jVf7~-QXk!6M1+pg5i1)|Zql@9`EE*UJJJEgDEzk%| zVLux)XL}QZ+Ot(CSzjq#7*42GDGG|8>yOb!Mo~9N`;R!8HC!?Ow7IgztS}!dlDiVV zG#unt{DkBK45=!OhJ}m%MLo-M!J_@ZVbG6@&D{T8WH4q+Jk}q0<%pCRRNl>8>lxi- z)QlR9&E#W)>dQ+I$hS!`k`~T8m|hd6gmN$q0|wc;YRNzPfJoCGTTJ0!LDiyUjULEP z=|bOdV+VWM9bWgyH|lx`DN?b+I}@UpEITbad--dmga^}msy+>_9Bkgfy0;LGlq|0T zT1h>Q3Jry^z1jn(RRv?-cngJcqAZnLM$e9HHj+GJ0C*b8R=K*TDUAm@A#?IZ6Pv0b zvX+r}a7!8@q-#aXopODdsY6)$MXNZby*+SG{yb903yBjp@u_%~_6s^m{DiGEuG1VN zuI+bVWanatkV|a+^GE84Ra|X4Z$}M`H~rsZqQbz^Y^37eoLDb_a-=cM0XVvYI9%$pL5qg>(`~RaI3_(l<4ZG!`Z>Bm}&+wzj9% zXxO)*ft{F&3OZ-jtmpf1N58Q#Dc@0JFe^?xvwb^Ifox+P6z5rOt6p5Iv4w5+> z%+4PlFdv~LW_&mMTk=4R3Bj+|;Wl6-2e4Q*n;Cd=7vl`Xk@mqUEqyptQo#Fo^%qAh zzzHGZ=E~h!x~$d~*J^jwv=Di&WW(*$k%#{AFf}XRQb6Y@tA$u< zOf;q-MYh=RSG&!YU~YD{kJ@X-KQ0YUk6CkUVhoK3w@k67E)b)xELc|1<}d0D(~p@J z#*_us(|t)j#n4}V!YUt(N4h>V9B;fW+o2~$Xg^g_P#e_*jtqXG;`;`~AkfBykx{|sCzHPN?&NLVVRE`-9>&usJg zeDkqfjHi-AYEunbE@kdz_xar=yNHuuY$ckE{Lw+Qc-M7bf2)eLJT0g$=zW|TURti` zo^771d40##_p6`|PW)EOfMHUX_F(PrJpaEL9v)jp4h&2uXN-ASv*2O|j{K@hzVf+5 zIkrOG(o+4JPb#xEII)1~EvLfOwodre9jkpK2&kXwVNj;{GaNs)m=lTfDB_Al(af^I z7X5K@*yV(&xnH8)!wsYJQj#CM0b7fU96}sD5a@sQ!;J4S7%2Q58n>dC?iz|Hf4+$T zf>W>P1jpVQ8ghhnAoFjqJm7L>XB72xk#x(;17grLaL0sm@E zAx?3n7L6a+ZouL-oYk-Cr8f;=R?8N1Q|YT4La-v53k7NczA&MBh{d|W;|E+OyK?!)!dbA4H}P`9C9{8E6^ zTeq>fayw`*qnv@IhxU3K8Oq~Yj6TEP5R$u9+vn3QcYAzqoPZ(BH6yCyV^8sOCDhu3 zWlv`LPuZOSz{#@zczxVD+Dz4(i!R#z&}svVz`z`In0~ot|D^s1+-D0EDL$={Ggx5c z@&$E-{y>lG{ngM%h9MkhZ&K%Vt+hx;$&DheXVNL^)A8+eIp%Y9Q(A-+JV-;w)7^Ei zExUkS39$3vKh=InNgLeU|CxXa#w7U6NF&>%vn zxyC*nbF$mqRaP7AHb{kk*N!CHhr|s80|{!sb=6a5&m<}EkfzXm=#ece_tzC`5kTZq?o46vG54@tQMKt{bMbTIrFzt?d6h?^LlPD;u&Pq;sO*-HhqBVN zP}3Q&Q6b(Xg`on@Wo5MS)p_6V5^J4f-j!?jf+%F-m=~%-Y9QPQe80jRaP1h5wE(oL zHc~!85zY3Ve7%oT?gPE2?m$Lz9?_5PxtXd+U$XB@zx5Xh9p%l>v5)Fz+%8%=TQ%@9 z;S9z<<gu$OSlWEoAUbJ}t7E?;qSkW1>)nX@)^;9eXLIbj#x>G$KLghgts zcDg!BXwg8QxLdwclWZ{2a#3A)i58_;3k+84+PSeWe&UD0E*aXjE6eBK6x13XFnw`x;@T-yc~7N6_JMhDxhI^3eH#i*+-T_KwAD zU&xjslO0833vEe}|GWFtfyF?sWqS6BjO$4!tG2ql^M$3j^ry{Sr(=3Izfg|k;G+>ib$B)~nMs4w>P3^P^_`B0^yA zm8UobhEn;{X=tI%BnzU1S|Aq*B&6VqN7f1bUr2$+4#ls*a>A`Z^r43ibA~LDY=k%P z&k8sRH_I5rGn;ed%P6p?pfaLUA2kOS5-38ASZvm`gcL$mnLk^KH8AP5Z63f>|DioW z##%=m+m{D?Drmv%d^Wk7l$~O#rjOvVwi_EAMyZV+>LzwCZVk7yJ?Sr%U1yd_*L@r= za^9TYTY>m`OCuf0OLotJA6ixt2H0~n!Hn(;S*tC+nHJ0J*NPHB^u@gOw=svlP^a(G zI9|VKZ}ZB1&u%%GKgoFvDg_T^p}7jzn;X}ekQ^y{diIwtb(Go>in`98cx6Q@KHgN0 zem@*)sI6vXx-QZ8Gb1biq*M;gkhGg2GuK#|{j| zB<4d>`?+*&<{O^5@vj$)v^>mXHX9N6Zzmhg|xtxrv{rKQH{u!79& z4MWiz*2Y$R*>jz4yL|;Z#h4@#!*f+g(Udp%6(E+6sJ|{JE_H7NeaW@r-4r0)p|tWz|nZB;SL=VylmXBT8sPH3wWa-pi#rC*+Wq5j#^m=hyG5a zN_w?kw=V8b4UL+Q@e%__Z>8%QO3VYa5?d7wrv+Pb(By3Afci%U05?G21=CskPwmRg~_ zWwR<1P_Mz=J$er_{IVhLwBmdZa1s%qc`hZX5mhIEZiVjX5h9vmgT(DNG%_mubZ_Yr zVGTIN8&!u{4YcZ(*2@-3Hp095X$BQ+#>|dXh-cBhyFoN&w54xAO%VQtxQY*TB*6_; zzpL*fspEC?IqN#1WP$FX_gZ_lO(aF+06<`+)EXD1Hc_YdwZPg#jUdwmtCfzZigZ4202w zQR?&&zwa7o)G+t4>#*O3h{a&UClUs+bG3Cz8H9e3&w)jXkp{ZmqJ@#;qDJ}pY$S#m z>*uJ48u+hv0)Km(v41#tTr2)w3)enjoHn8~#rl&Ic0l_I&k!0pq`rG3VyyBpSNSYCukXoe+nQ%wCW$ zRd6al9mYWCxFfxpi4K5fi?7fPga5wn|9WCd7EZ+w@KSDCvV~jkJ|cBPZrn{>T!R=jv*IiCk?wGKnAfg?Vi=Dzg{c^2k=nutDP{dz_@W;*#WxLDjpHbTZ( zsG8{-^}AXXczh_*fo@ACAqaRt6*oHo^yuP!p?LNryeIEvsnEtGg>ZGCOvAi|?7zS8 z!9KC@`@7X>9i$Av!WEI|YgUGN&eNH${v z1NL`^!nf;9g=*)nH|T!FG97Y3f}DafoIGFRz<|IGf{8VhK0IH=Mt*g>+EOl}@(*m@ zM|bg+?J6|`yBx!F!K?rWPl6Bp2v$T*f?1cq`R8q*>pRftz5mJJ{pRH!IPSB}Jm>fz z>V0iU{v>#i@u+`123?ETiA+rdd$j9?Z0OtOYxUqQ2V0qlCmHTezHM}(g!PX>l48Ac z3pDD%>c-9u`se(74vblw#l?Gv2^VpBJ0@8o`>1A+-+Q={=8-V(0}7a-o< zu|UFW!xz!)d8Y&9ughcw&t%qL?*-I}#R$zl?`Y^C%ti#*b&h6p%{cNCkRTb@Aiv^1 z-wTgNi6tj}o3Cl`6$0LQS5i1PYHi5(-`fwMJ%*kp-xCf6F$A>%*D-qTf0PKda&OD8 zf%s5yAzQ_Cz!-4c`vhPJXd=y4D>5+?!Fa-JE{mo+LP%DsgS)Jrduy~q5EAvsFj2Uo zCJ!evd3k~8Ef79hG0QL`>S}yis7p`Z5F|(D?GOYa2WiBRLqC8KLrZI1@wgFG2$GpC zzIY)AtCEMOZh=z;L}>JI0z^hWh<4|MTH3o60%XR4!KB9>-mWCx5>zRtBTYL3rv_1( zq~6CkDji<6UkJQoph~^#!gI9-Mia~%(#6EBB0y1iyPY1Cbv!aF0Cqu@N{tF+q{s}8 zt6U_jQloK7Lsdf`&rb)Y!oFtTcUthK7P^UaCAWtYf~a!`ohSp_*BkUgaX6X)94~R) z+*2*S2V(vwNiza0TASsQ*L4%-zlVIlT4#)uBr_Ly&o6%c*EeF28OA`cyg)Y}cn~v{ zYW1TA#)bN?-GELuX_xXU4S@Sz_-0J86!~f`w467GL036!O@p_vsrHgj$j+j{$p_FU zZEt4ggb4+ogNc!4;?BEGBy#t-~$~i;Yr3k+w(Llk4{!Awwgdi8edpjt-5}%JZs9tV3tRv-ycvyeJITiW<^}n z$kBIBG0Zrbn?KN|ZwZ{rE33(2c1ASLEn~0GYsw_!_YEhx;DwD#aXTq!NyGi#kV$lk zZ#Gg_cqla_fsaWSZLnduFr4m{R(G=h+B8(0jt9Dr&7k}+oT|e~pi;#Z^pK#mMH68B zv%r-+0b_NKDuaOw49VQ0K60s~*O1C9jhMVCt7y%`YOM%;FUs1iG9?u+hfjH)-#nFw zVllju@khKjOfr+015`=6EC|#fKjtyGP&rjJHX(R?l0IvT37yy!xP%*5J|=qx%BN{L zBjb%Bk($5Ql#YdKdV!15a~!Bz|B#_ilP+)-^fsxhPK@AmKq*f3CY*XrA2APgCl1Rt zUaNmPxh&OCKxLqOUpeWBzP>4=a7>@W!!t`6Iu8})iwkzKYUkw_M35vS!NYVeVo~Fm z-#B&e{i!$8nL&-HsX+kKd!!;fstP@;S#@7;boHQ-;SU2C8giH&@P;+m=ngwY-GG|G z;_tA4J99FVyfljEgIzmOc}764O%!!=msd?-Ks1sPN6Fq(nWuQc(42R4ZL=4hc<4a9 zLRQ7%t3z|+`h+`L7))3e$5Om4qdT2kD0G886qZ(`VjB*Ab*wtD7@SO@52v=1+;!am z4^9@#SerW0AM4wkrbSQnLT|@>h`&Ian}1(*ggrQbv57Ts#7n8*g{R!)O_j7taiodk zMfTFXQ&7zS%MvsyFFg<&%&miQmTQOq@fC^5JUTLUOFqwh$c1ufAo9!|GS;`Km$Jey zxn8>tBN>S$*bLK?i6oMBg=Q=>n7JpjGC%`!XA0rxzj4u+W&9SLusJf1CcK2cQQ8FC zM~Kdgsp-}d_@F1g>XCKO{scy%dP8v1UoBmHoO|LzZmdiYA#kiA6Qts2-rBUI2pfy4 zuoOT;KZ*_ulf0?c8M#3lZq8~8gKn}okx|6d5^E-bCc6i88c1ETvac+R0y&~|`b-#^ zpeM1OlDcz?6(wfkOvImfewt#8Q3m{=`CDbX=N{*2yqf7BDR$&T<0?}CK2G|Osyw>I z4)=SLTx4kLOq95NWYzQQk1^-y$|QlVdZ0j3-F=K=--2Ao@72p}SoGB8zfVLj*jF`S z)QoSZ9+$Xh{i*Xl)Ri&bPScJqP0!_!iC@I`royeJ&qpoQW54nYt0@DZ!p}dZmb*Xx z*qYP>=T!CN2QU7~i3a(0y^PRndGm|5LJV!Kd;ayZN7ZE95&K>CI%w+B6rTa?dTkKQ za;sRQXg2$JW2Q%nh+(?5_pEoAyaS7!$$uPK+<$Cdx`&Li`J}V_9K3}TAe}Rv}dFAwq>u^e@T!o zV>9V5<1Oj9s%U4oT+x4{iN0L#XG!!LU@dKJU|LO9prK-B(QXmYC^E>!)OyQ6sIR)LF@ghyZ{C0(pLaCmoR|dy{_(P z4%_*S`dcpbY#b1JRQrJR~4mQ4RJ`A`^906 zlYFHW;^fr83`f+EQ&7~06`90ZUl$l1T!v9WPZCs7mOCG#r2+9E4ar^wPuZyQ_A;A{ zL8_=P|D!viHCaz8>I>;G!&XFG$xv=wB4B(EzoiNKxhI&$f)_5-K?fpv1>KElH8MB zOq+uf zTIAa4G#)%B;dSd@GMTIVEPlv9*v?3QocrGMY>>56?WDA_@{(?B+U&33=8wW+LED(C zQpm+pES{A~k|dEtt*q#ISACo_r8q@)f@okB!1i6Ud?CW6rDSusXuABV>9Jit!?BS9 zO8xNjy;tZuGc-s^zJi!4dAyp4>A_ROt2LkEHo87GSx>u+2(MO#NuuL4?YGL-ag2@i ziC9vMBuPRb#!3%>V~SI;zWkAY(S($EZaD}f+bYS72IiJ;y@ zb*VUuXh$oZ9`AC4C0w-_T1#qGEkIWA^Q~5}-ShV^;~eI4OQvgC?dSLelQZOuEZM0} z$B3^ZS7mqv%Igwp+3Onf{GxDmxyU{kp{64cjIzoJs{XW|<=|4k z(wO~}ONWv20`{4E&F5gKYn<)Lxe-h7i2cW~Oq;9CeKR!7L&=->_y&WImb-<{@modc zN9*&sQ}=}vG;hg`#~tjF=_3^-hfj1t(>CjE_mtjwL=EW-Cx5RRSK!CQa|h8v3Glr1 z-re-d;%kqnJgP7JwN*T@J!$jnFN>$1)(dI&6~B>~)n}8gWnG#d^NM$MNuZ&{q5whd zLM3-m?@9O#WkJpkwAM6wW?@B7ahBRxx7%WWx?NvN7FXq<(n-89ASYX;7smFoR(R9; zGq+Qrr%V2kZ1Y-*k#eAij1EmyA|7qNzbtH23l7tOO;zn$P-#Z}W0=CF>KrjUYyV*9 z*wN_-78CvBVIXN+UIlBba$GzN4|^~vn28ErPRDDso3?|o=+%aAKiC+W{pXgcgNt>= zc}b>8Op9T}BsGF4&)^J2%o-aWR*lkS+L6v!$-hGc%(PVY@F?}G+Y5ZG=TZJm}D$-N6|n?A$8LlgD7$N0j!_|9q5>+h%1 z{$d~b!3{%1pu~cY!CwYvbnF{QGK`($aT#B>txXJ>_iPXcq(KCr%z_0D@4^kg5g^Lf z#^bTRh!}GyFum1-1l$J+UIc|N&Obpfe?g8;7*Dnv!|n>X6Yzyqt!2nYNkOs3(PBG( z<8*J;@QojF#-~m_mh%VaA`#-PN)c)Mn z85t{dNemy#O0+K-ai-21k-{Ps5>Q4Mih%-1KPj<-CK!)!d(M?Iqk!cwG{zJ{dD{eD zMMaL#0vR!itASy=JZPkiJtJ}?SgZ(eLta}z#-zz|yJa=Hq?g_U>uRPufXy~L{ub}} za;)sF3F`6jkz27)Q9}{~#L#PF>bj^n$y)dVQESWOb-N4#xGVS(0=yxr2HX&R^#fjz z|3~oE548KRRq_AU{fC?=8t8MnSgv+9Gb4ZURr4oFr&=d{7nm;@lh##Lk-zw1yOb7! z13!5OU|ZsgREOE?{!0i2QORyO`He(YDWDtqSre7?Uzg+*PwL)dG3k~}XA5pes_X0J zRn{C)ZnioW({d$`TTAGc;J~Lh9{u_J{IqD*t{OYa?vK-vqhHW;jL+it#9ZUyUd3Aa zsEe{HmlV*(f|uyZenMl^hJss6yoN_zKY)OzH?!RW5+f8aPKB54mj(|#x?Wnw=`&#*cVF6p2m}^&$`G_WtHDnLZ zGZ>v!PeU9Zmy^1FfHMaX9j z;2rn{@~t9!vVA_rdg}jg&35#6J@U6%cj8m~?01SAY!Ry!bzEP=$0^uAaJei{g~^m6 z=`w;p>eL(uC*zPu6Eo&m~obdGV{W8@73s2_$=N1^U}fp=3aeCBY$bM;xX3hR;hj{ z*fNK^sB^EkR5k5$-eVp+wWr2B~H0TXJ?NjjbQ1>lm=z< zoTZ{Y1#punru)>!2Q0+0=tCCJJu=;xNk$*Hq2$Rd3V1zd0)E|muR%$eu9s4UNUb4T zHyKzu0g;Gbz>*?Md1|$yZ^i`S;>Aiz@8iL zeJ&E`97jE5MXzk37E9??pctpR1@I(o=1IeN&Srj^{lMGpk`T6*E<^E&pVpM#dr3R{ zg)HW}==t1mZ){Z}X8DeBE4Oj!JZ|pesPpebH)8vgW67sF@fGip=w7WGxRhZsS((v& zan1en_TH!MksoQwed(FGWqQNWb!XWj)tT?u%o6E%`A+~~aLRVX&3sIs^==w`yfG;~ z(~W_3;+aX6{&|8=t09J|aZ&QnC9ipn3R=vJ>eL$g)4|sa%h~xULF^*mO}m?CZCIwbJM9|e_{U6a?+D6) z&W0MsLU-qcxQ9kFIk>NQCyUHLm`216%ucExnGeH(C*544i(JZ@d3%?N&g0X%m$tgx z=5|*va+s(WoT+$HrnXN5K3}JOweu{$X~^qD!~QVRJr7VCQ!IJ9 zTvuHM=8ceXv7*PJ{8RBT%zwSWp-7FBL>sG(bz;p{`hO^U2O!;=Wm~ju+qP}n?q1!i zZLYR$ueNR5wr$(CdHX+mpLgzw_s+X<>zfrZqh>@_#r!hItjNew!@fVraCBoNHIT{P z^Fl-hOVVh*zfP&eVv$TlQptsozYK-WIK&~ip&xHf4tMDVq?m|Y{Qk5r(2n-%;BY-( zD1wJ2Mn1Zb`A$}&)cJS&bTssmKf^&C51DBJjL&r#X@c}>?MAgdAKBlcY0~&QB2R2N zBNR?Drz*-*$K|=r)~TYasWJziKbc1zVbCFoApgil{NJ~<$kNcKZu&f*p<-}6`mcFL z7rU$H)@6rrhvg&mBWr*+$h>+9`=jJb&&8LNk- zvu6{BbRongqM@98;aA=Z99PL`w49`}w(@Ohr6Upm0Qtv%)VBfyC`f}tfX|DV<{A8Z zt!+U6aq~Y5UHs@^0odCAqu6Rll3q|+o;GA@ zfi9fkkRpLM6Q2jFU51JvDc6OgKznqyQ0Evv z!QF%NMFRiuhck*stEQOGGJ$ZdoYWi8l0171d~H#ArI=;={dZ0vRWS4x@l`O<{D?cE zTqaKwG0U`@kRb?R7gjb)UpDz*PL;T{L4q@822j#0r=BMQYVaH=w$a+5=(Sa%b24o)2^Awlk=MeR@*IlxXLP z{DWxY@okp(qtcm@LixV7RkEuguBY^J&HKa8+25vKbm3z?qOV^tE$Ea}xhQut_3Tm$ z{UCGMg>?nGtO$RnY!p4NXF$xAk@EYe6dh=pSuz^C8`mbh`8Q3O66=}Jk$5$|NExLR zp~_r>wZD8!9GSn;nZaALD3*+txnlL2&A4TAK_FwKb1qBKvzPKKy*l}fTHDTt>Q-&O zx}g2{)pg_ym8#Y2jhfB0{#>q9ZMZnx??Al%CC`BD_V&QbN`vq4;!!hWL#gG*^=MjyT`3t7k+oi zE2_F+lwRvXUR=+AJO{H)%f2p%N@ntDz;Th?X&LYqd#WJ$A|8Z2FQx-sr?JfGmOkbHaS|IO>WF#O<5J; zX_kj=KsTBbPtz!mxh3m`aZ5{M+YNW#^zOG`AJ}1qN_um6v5$)PjuIOUGH#2YI9(Hr zdPXNS_p6tIz&@6UOjZC3-zM=Is`3+`e=8}JG_%7A1afh|F4tw*{ zQJrx7?1CMw}Y64*(SpSLgZT08Cld*w(T6F6zkK z-fGMuzhX@)Xcc7@wKWAFiS(^hmspp8bSF`&hjzy=jqxM_?RtW`{?^i#T#jbjs`A#P z^U04ambL}@H?uj9k8WRYdAAB{V$+!biE=nviNFLmt-uTB=i6&13 z5NpTC&zERwFa>P3Ak6hje}TK&Sp+QhUgb2T%ylL7W!@~jyEmY=2v%vRJ$pN%+}oe&UgBI3=PC33o!gAR(D z6NBNZ_QG@2S!rc>1G|2Pv|W3pI;^d&t~l~pnBDww{PQ^yCw|z4yoz66|0gRrsx$xP z)dfB1#0i_0G-uYZlu$5vizWJAm|Si^U(*_B-J1(3W8*Eqy!mZ}r+c6nKd+D#^b5GC z0h<5(ZecM3nYX8^2X>`~KWZb!PH6QW+w=YYu<52n5XIjRpAIPvQ)kb&C?w@UmH|Wr zk4!=mIhJgIJcu`^^E&`JUr>dd${_qU5$@hA3()F#qK}b=e7XlLN{>TT!;hQ9W<6Vx z^c8Is)ik0mZ@U*7|yc!^8D&T1RbI$cf&tw-^NS^DHrcwUHP$$@OY6 zN$SEV@5LJ4Ao?BVD86FqvZCv(F)d3e>!~IwT^B3YF|X$Bv7&g+b%{u zL8$PRscaJCiw+KJyjh^!Md!I2rJ*7@-k{Q=G*EK1gK?GXcdzW~E;KnYmA=xtwdv)P zmWvqeZAT50gw%2Z9?;?#$`o-}P^s z4PQS_{GVX|+-j{7`fsOh`9Fd^!jDr6+SRX$e7&|!rvxz8B)wWqdR1g~p(6 zkdoxTiiJQ~md>q9NU(PL^3#f%j?)~<)&MhA40JVCu5+%!P_%f(vNze+vdyp5X-zrL zsLLxcn@Ujkb8;+{ZZv`eJtwWn3K?w*5V-<9wua|pq~IRqh}L>eP8I|qgas|1d@qHDPxv!+cGS)NVjpBP z1nnYcOnw=_U%&in94KjEYwBie~DDG?UO?QD=J>1@k6kwHlCqf{n zW`Gm*&))a5eKwkrFDhkI*w5Y~c@z{?4_#n{J0J>USmsx#8G8j6A8f~Zc($2&#%m)z zsHFAfIu|!vcb28xuJRi2OK{_M1D!}Eeors_NXuue9PtWNk16^lLyEh5?_*dViZF7BL*%SpJH!Pbf4 zD~cxi+G`S%x8lH6QwvI<)<2E-_xJprRAAjNp}pRMV8c~v41yYThB#N?_~zUB``FpA zRZQ1iI~K&6eNyAEhPb-c`S6h080oPJmysmh7gcP*Job!eMWfVCAw+A18%CPOPcfg- zV*nIf(-?bVc=sx^NptB zOmp9i0tQ_(!?Lm3T|6EUak%uxpegn1)bHdy;a}FS1=|z%cqh^O_MM#t+fAJY@TSY0 zsI3u0J8Z?qT_f|JsgC1F5VHohf&T68MbFs%Fry^+&+0qZ34Y6)`_tIdhWL;h=)#Du zXr3m*hi@5$*p;evabvYDNlWPjM@rTyQ`x#~uQeQbql!@hL?5RLcOT+`aUjJcTkNxV znSWzYB^Bu^TX^}M0|Php6XxuFFr{!EBEbQre8+@w2f>GTNu6fTs z>?v845_L{1fT^7}U{|l~k!sEX=}di(o$(?EC^mW94>fVO#5#?-Es~Wt zf}Ou>enmKn?;MD-sX=Y7QRMQ@yq&V4a$gIS6=XST*}B*tk~~zWalz&IBKY= z7>8lMzah{1X`#H^d-TLD-6oq&O?vPB(aD-a9ni1;TIk@;(aPf6d3tbzG7==sSzntg zV#elQ&zCD0*?cLHH^Hj@6TR1Ru;||$x$1^URP8CFK;+ME{HblVM04#Sy~9tuV)Cg9 zqgI4A;O(S7HMuo_dudOqDNB>@!|+%okI&Vhf^v~6a1>MxK?mgB3sf-f8_`Td8?f0B zu2EEYL^Q9;c2c6&bDB5S^J-xAI@o&x%i0#F_dP)bcxsrW<`u%Aj|Lv9l1yA$jrxnY zb<_Ak<(bze5oC9S2@aT@tu0BsxV~|B630b;DY6;{ieYEmPfsuMm0!kptwwHcc~*iIXL*v)`l)kAv$+(}VdQ+kRNfFA&*X8OKTZWXCe0{`4)%h7YG(B15j!W_yLE;& z)0*Z#YK2*}um#aO)h6}?@5zY{&@X%Ft${W_WhsU06zivB8ms=p0Ye_Y(5|F2FI+xmTksll0 zd@FRL4*2pHc+XaX{syV|6n)F*_Ga{$_5*=}7#N}S99d#B`@N{)!ZA4QB%&ea;QR8C z8}O4~`S-s74Dw%ZTZwxKR`zF8?}`Ti@N@lZ8)WRLZ=~<2PcLj^WGwmf@=fi{9o^^* z^&LA>!Fdv|*L$YzFETsfFjuE*!(tK>d4-Kh(s|l zgCMT+AC*71o}WFopYLC}vxo0DmK`>qom#KY+q#}x0oB!D;$rB}L>9m(QSgKN>$m{a z)vz&8!pXoTVgr{H$#dXrL14nc0c&PJNeKCo!hH=$!*V!i9!M#$fF)u?wu49%DE#wg z-&KY?4zk2J^E)hg_iqWglo0J2Abum!lEQ^brvVy)FYNA=AWj0{?;)H=RGtNSrmsik z+j+hgoJBqt9_yP?`R>shW8lC-gD=h_mQY`SHOHDnHjBv?wVGDwIxfmz(DctZO%cIi=Z0-*9Nrsb!QU5A*hDp58!@efUM&ofMfUrLHQBUTo7dGDMP5(g41(D zfb9WGP5C0hOCXe-fqGXE5b0sRxM>Ii5h5Wl-dKai21^Kf5^f>b-AJNbfD3%|9)HFX zAP5|wcXI&3sUd~J4FKdpqyfTuJ^P7(5CKP#gZ%aruvOgm(pA?hCP)mOlX6golE z@gVN9tXoT3cHj)DVt${0;C0R!*!ew|Z|)~7(SishCsej~r5AF0%M&oiroaxW@U9Oa z5uOJK&@nJ^GAcSlWUvilU?#9|;d2rZWRJrFQU#6!O}ZT|PK1F56~qF8QX$D06WJn( z&gbBf@APG$swZerznEi@8`U)%KKs5&7@bjyL7sg`cs|!rwIeCo8_G&&R8nOqZ@;5S zU2b;hirhaIz5Ed~Weyl_0v`_chxodc?5;V8aP?+tpt9pM7jn`%R6dgOo_VEod33Vn zBT0D7xhGo|tjvYOiCs*|o>@BITb5)VxNune=lfSXe=f|1JZG|hG1aSMKOOhhHm1i` z?Y4e?zk1t$d32o>rXx{AL{nOaqNI7(dOp1s2?|%BXL%q$raWB=qS0| zr9RaNic89=`BztiEFnhpx&_rey(DVKYXV0fEsWp0)jy{x4e;!aR|*41tDQ$x0CR%2 z1reGPr7c9DNB?CP;L$rBS`Y|<5VGfRxW7N7A27*_5Yr#cjv{sfrg?pXDq`CMhL>i! zlFnz@`n8!f=vw}UlOxdq(Q>cf`vOQBr{dj$mzGY)!U{r!MwPIi)CBIJ6&7UM?trJuxIg4RWzeWbv zIWNAXv*C>cbMmrJTzfu5jDwl9@^#@TkTB$DPz4CSj;K;M*MqykCPwa8N^j=$i;|K? z93*9mFs28dZJ41agA}%>j11-w0=LAW-yQZ@=#9OQX}9+7U>Rw}#MBn`FGaZpV33L1p7s z(EEL)2|>`H++;YqNOI&AK7IQ~S{WMIY*_T>w>LW_8F(!feLQC+3BZ!e@a`K-PK6fb=a9oupwpuvK^AqNFWZljHJe2Hx7Ajo?F zBjatP6uGZnX1a4Fe@iTzSEyRA=zh6$tNF8V686Ao3N3@u8m<*~LF$MOM41`JDU$J;gZQ9L9gDR|2L;_jAJ>vHpY1cueq_XPrO zg^$C4fWtshApaOeplT_%{f=b0Z|@y1p5^YLs3S5nDmQ8!yziSSp+9txi+KHKkNp0V z_P3+X72ynWz1^#2yXE#I}BQKyYHe#V4u zc?K$*sGjyDy#j((O6`JI^w2R*$CI~94X@4?A!MC(X&SNvmpfe=CyjwfvCy4ya7PQ! zOXr~b;lW(UflH?xr^20leIwpbr7IWCDb4Tz9xKV>IX|!3&LbDzm1<~rYLQ%K@EP{T z$pKA-w5WJS2bv6~SGCDi?O=t#esV?PIor7Y9s6+U}L1kJ+`XANnJdHt#FyXBGjVsq- zR@#wno?(WwD~`~;>mTx*(wv&kE5{Oc={;U zCNO_Er()uq0k>H9e0Cx3`~BD9;xJTJ3E`Wyx_(s{%qCywa{n>iZv_W&IEdrh)JN_nTTY#A#q-{x$(eE|Mlr!+O=&VCFEQ+x&i2x2=hh01 z2zzW5HMAr022rRk4z;<;3CFP&@U_v%9d~iTt1*nJ{bmrRqAp&Z%z1CwKHCerA1?=%|DktYV1E;aiKNAWYt&WSRl+Ox`~aV#=rerp^GOg3+4p@FAJ+CB2N76A z3xLB%0>BKVlNoHEl$hqi{P>*I2)@V{Hw8$@ISX&c`zZvCM(VmfDRc76a$6TA$cJR= z8AT15JEdp4{zOymvKTg{L&%vAqsL-fmo1P|4+dlJ!LCh_+-K#+4*l~R+ZJ5m9ZUfN z%omP8fGCqlB4?)q8NVXE`&y(Y(rKr?y>B9*gsxUi>x5iks#f_%e>+3tc*czm&n4m{ z_ZU=a2;p~!H`tw`*=;@EaV>OM>xJd{##kIR*E4eq54dY~nqB`0MKJyH*VC5^K9f}f!vqRw+s8To6VLDtYhtezi4_c@v=_#aYG%a-A@!O-xx)?Orw{3_v_et=e=?MtgMI zb&!qlT09iC&?kKk_*)uBBSUn66WmmJf#aF4<05zPKEV)zlENWjK9+3znZ8pSn{`=- z3Xa8m1VTb!vS9eX8yHxG`}+Fo3Sm!vvCay9%zO(9dCAMeg!W$-0C%r{h;mT^{6%{g zZ2Kl~{0?)02bv8K`Gx9rW~7?Uuzl;1ReTodmLr~A>Ba8Ha;ZObcn=gDAyt(Sw&zH6 zH5|dmyqSEPIkwrUZAdvl;n8C27PoDt|r4p$)F=nP*>r3txfcqrFrcsPj9sLrUNvcK6JP+lo_gVJ^V{HHqbw~ z6eWvD0j(mBb`&C{!769s1ho1ny*aupa{lo#_`%#vE#@xI&I*{sU^C){>%oR`PJd2z)TIZc*iAZ;RSG$(xZNiAH@Lq^Qj^T);GP{ zPEb+}$-QhGCk@uQ7PgDg8Ro6bwCio+U-ZoniIfyXo#SM|=Y+Difs`zJ8;8&=x{=~B zLDAD;9NKN%i6wVhwX`Hs7S%1ZE%PX{DQMWHtri)X1U%#U4h~i4Ueig{6=tgy2vsj7 zl~V&jZ@dmoX9ZIjEb|CLY%R+NTTjF;wHlUFa#7^hAt^ZWmLg1;EzZRSGSr97Wp`1n zx9JBR^dc3@#*fEV61OK=Oe`s$Zg0I%nte#Z;R-PK1|B{9UG ze@O9~Eo^%Mk^%t1Axy*Cz9x4l^Yn1veuG{=-K)r6P|qiHG0LLF02bIHanw@8`iwVZ z2YleSia!4!euf*mZq(d1c1b0%wGTxS{?a`9CQ?5{;3iFtjkp1+zsREMJ>@!|5_TNI2egHo zKwykk_~`YMZ7U8MYg`jVvPQ|U^5y1Ca`X|Hl#RTv?*K~EW657i1cb70xZWIiQTgf6B{3k=t8SCmo(9@#9fVG1ijo) zZc?n$;xte=3tB!f8|Ue_{4n)^0pQyq*QaldC~Uk z*8%P`SGKW$HpboxILN;$lf@S5^cLYzHTvM%4vSWXD%w?tl}puqfT`)Fj+c_Oa*JJc z*#fz!JzZVZil%WFi5u*{jev!5u|`XJFv@_kO4NTmrLeDDaP1qE{iR1aU`6M86a`;v*^JP%=e;ALY7>uJaw?;y z1)$LZ8lmmJ-XI#!>wajJw|(k&*fa+Mb*~Q~6b7;%-~Aq^mGj-lmPzq`Qh0cLya&|J ztNMV|mI~LMB?aTcK074kFk;CEQhjRhO5NgFu!zT8@!>6p>icJu)Gg6ClukY%>|K)NEd9u!v~{& z-JJR#jy^5OI;Ydrx^<|HgE|bQy+&P0t`ha zb4_P?do>FW-~D}|X|v>6yc;YWxVa5KD2I|1l=b_AS~v->eOkDK0+}xy#~ySuHR#!g zmoM=|#nwj^&f^hvbI2VJ1FOG`RYs}r|DFlwh!ZbJZ5mFsfXu_J+`anp#5Q$$D4Uh0 zg?k#3P~?c6fmRmwgbho_`1R}aP%e}23g7ECT>2*L@E3bY3Jx=4CUROuy^%AiO$c8> z!N|c#r|(3Gt#-wTipdVVSKO0c&yb;xV}-F2GTCDQ$wxSsJKFm6A@`6V17`LvzFAYR*+T5J)h!84eDIVdszlv5 zzw4l`iASn?V}PC<#R1D_+AD0H*^NU}^jAgrG*{ z$Y$d`WiCh*`fLytrN*?{ts(v!up$T8>JDJzi!?m}u14~1yXMq`sf_x%*e`DH7gwog z{yZbV0W#cya>5G#-08Ib2H=>DClQc988PAx@j>(1-J`@Z^sj0oeHLotpz(f&QPml~ z^%C~p>dB6HY}y*z-aEE^!>CC!F?8Xm!E{DIyvE27J|#;KXlOMlua_2dozB$u+B4=} zjeK|;{?%djT2ix(_t*U)TmlZ}stNm&W?e~6=`gRaI{B+Ga7^u0E>H;$CxGa?8LO3z z(!N7?{k;#Da)UZ(?3<>OK0sPfFgw*>=>6}itwYIZ)^OvKiCruDc%K-0Ci)oHuY|vT zT}ANcIvhAnPaT(l*jbujbU-_OM^Q$}^i~~Gi3_im%8OZd@(f;m$@9WPd&4|B^Ii=%_iE*{C&V5Qq~I8hq?mCfs3Py;RCh+ zBCtRtfXRIY_b^9{phYZ*T#%ACTasoT-c|FqFy9ZTsZfTAd~>)53qqkn$~scQp_`m7 zvh$yNlV*!2VFtpjbBhUiO+I+~LQ>NSE#nM53Yc?F*Us+{*4{FLt?^NYCc4cMr*HWW zQ_Xb5Y_|jr=(I_xTd~_Ls0h zsD%}mLJHBQYR@ga7-1Y{J};w)*|gMH)m9Og(>Z;1@?UkRYgvy zXerr6`o)QYt5H-sY|hN@P+Yz?UE!jPC=S(rU{-U3OWb}j$(e6=+3E{%J?z=#uq=VB z5(Y@m7WD;d(m}AD+0kjRTL`PF?2e3yWy*{vHDGYiE<{ec{^#ruwCrZqUtB1)El{7- z;&}{^K!|>gGdneUnzcxWKgtb7kr$&hXN97cH!2*}Oa2xS651w$nf}R1rF*1D<8w}9 z7Id*qT0M0&0SJmpQqfzUo2@QHtbOEL;ItXHc)Hh|TuxOqw%*SwO2pMEx8`IgcyLPNVawKSP1K1i4tvq|-*P-_FIupqz zS*DD1aGJV0yt3 zcK|UEkP}1@p!|IqlwJrVz`#TTsRICz#HBa^ygea)PQ3%qolCJGB=o#J(mE_Oykx>W z{d^(l$nO@AIHrdc5Gsq|;v$nk#vQyuaL~~=m*XiYw3n!RJn8NG8}shn+gdP>6z8ug z%5y4#EK1t?c&biq&|Y8JO!`#A!zZ8s#M5qh=P!5Nz&XqV@2v7~bt$Q^}I?o%rbQmC}7^h^bU9JGD#)I$&DCL_Hb1pwfo;lIN| zU>Nv;(ek7pF*%)KGn7vkrq7UW>s8Vr-xtqzV6l5*@sPa$GlIRr2n?95brG~bL?LHL zXF1(T5MRc)2q^$10|qGEsCIq0tq1w;{MUS zlhXMJ&f00IRs$MjE(@=fM~&F3-YX|Z&;eAfYwp42!O-u*Rp#@EPbbmBYNQ(4U&<1$ zBV6<>3d2?8OV^u0(7RHKLtu&6&z-q&kX;higQljNNh8sM>mN`=>7HGgTXf2-jh zTzJt#z@v}`RRE%}-5-{N1i}>{(DUrm^L@HWK^Fpx5eTy#PxbXhfB{>9ho&TeL1;6B znveWEQseB~QV!LBwPp| zyoMJ1Zt9{h2(+y-kBH2RKYQ$Ke^rB{PEAto&D6GT{F}Y}I~-%wjWu6%HTzg(6tP>n zHX5cwx>wjTnYaNDB+AXxzw?a}-`L-xG5XIMS*$l!z>iL6R*x~{9G~uwD!F6z8aa@Q zb;I9Wlp;{sO6C5(rTGyV0VAm#OY$U)K%(iPs=HDcn7Ro%N~-(t%dLdAM3#AN&K~rK z@S%@AfXn+eO;Wc_?KJr@Cg)#G1#v|WrZ86Ta$7jjXBsA@rJXsu9DN64WE#Lk(1>ng ztMfX8-xY1{oEeL`-Pd<%^M~qj%U`hW3lK2%aK8FW1I7pfCq6nQKKS_Jae`t10!}2= zF@!gOJif2TuE+t1M&j{^J`JB3T=6l!ND+ zGenmu^5A!e28C}({@`r|-2(_9z&CuL5CGUX$-#`xD5*R71wp0OfO_E95RyT?H^z2`dHaZ zVHso^)Siy%M6~B=dYfzy7(c*0HLdr{NF!(BK(YGGTZ(Q(S2{8;HewZEZAzv!SQAk; zi19vkbQ=wK|CA`j;!{}y_Zp6Im!^m5KY%hsX7LZB!hjJnL{ez226aR9g{J$6ZF~gy zpbx-Hy%}Pa*!pq^T>xrs2)(LNUPM3>LI`|>1JPL5ujYtVizucKrDUr8A#QGM#P9tS z-}Mk~GKiwYc17c3-2&1ur<;)N*;Mo$S2(XwM4~eU%0q%s3`7z74HOf@HrX>X2840K ztgJ= z$h*OF1$Vmts^`IqcXN|&btSLc2^diC#wQv0TLI291BavfT?J~FrPACBT2xKU*nf!C zx(!_aiX^90uGIR789<7K-8|fSz%cn?)xal$EWceEK*EWR$<%AA>v7|F+Fs`vtIkEY z;ZL4gVM~LX|1d|190?Vj{KJYJ58KbtaHti^Qjlq6b!BKMeQ(EPUMF-#;<}*AyjM@n zgd=(J1t&>Ws+r46P74x-=;WOEF~r;lQ|Httgj>|~*&GEm*WO!6zi(ho&ub%9*){#9 zn5xssVMmfY4q_H}#-mvj} zcck%eFpPf4YTk^(m+ntfp)J23fV+sPleB37X-+W=lWKvx{C&|v*Yoh7tWUi2Pn>Jz zGR<bQ{WFj|j864Z}-&=8s7fIbWeiYBj_&tezH4q*wJ9>56tLf36T)TZR;EQB8ja1w65FQZwq%hsz z@j)J6PyW1wFs|DbSvBmQlj4NpQJH@PIMqKtOdtaw%&-}r-u3pbVdm>|oQ{z%Pb6$1 zt^K=OW8!9UuYW%u2N{qsj2}*bKtcip!5(+#RT0RyF9oNgW5ZZfWJ4kAs*kM&L10%% z0ORloO8uiE2*2pP6K(A6rr}tZD*OW?Co+jK8*5^PaFCpS<%t6??lSL8$n7_BlSaOd4EM7?M`E07zdG<>} zfm$Pe@-RCVgOP?$)(%u@aY{9pb7kq0Zt>Z@lT24L(2SmRcV@k}?@|4Hy8$jh9?sp^ zLK(*+5@9@jv%zy7FdE=>eVZI7m<@OR8dt%+{_s*xzzo=qfqcv>60Xo~JN64RNs49US`^&aY7kKo6?{*T8$Z)shx6j=V_6LL9K3c_wp z$d&J#w^D`kIfd}xCxe1UfZgi&0Ui{^u@mmIc5>0b^FJ)+FcJ*E7iHi%Y)voJ(Ns6lmP(u@k()qo-j@hr7|(KQ~XwZf(Bu}XSzNDV0ulYADZY#hxC*I|$2`rI8L zzNKa+W80HR;`uu^T}F-e;@1t`mn!C4wfq2w@rbv9zc84erhmv_L4!(HSC@~*8bnpK zU(|lcDLdrj#zu!Nj(0NLv;BRs;~W4G;g@jRH&NU7RJ%R@0Kq6PNt=$`A*N>Tvq(~S zz^_(1!?RZL<`Lv9lFe2!qne5n5nS3{iZgpMpODAeietwkZ=K3QhKP;Yjbvq(^o@uD zc%`dI!(=zcA|qs^_1gBQ`inC7e6ag#-CrBvs%4<;0c5MytJHNHvIc!i)9Q^U>NlC2 z+QI0VP*cC_wgPx){j|8WngDYj4nxzs9?8@vgNwC^B&(Of@DEF#7?2p98scyW1I@kKF(oF?3C{9A0WE+XSRw=`D&a}DP9_E+*>f3FbgUGmA_s~x;eNFLu9ILv4VLni4fO%roQn&q zA6Ssi6uCGze4ly3Iihs@tF6Mkr_Dgp-XF?JJ}<@RXUC97ntT~f1N_;(<&Z(_`X(jD zS@Rb(i5x!GR`0c;d9!&B2c(VznW5x#7P30FZ1sjTaOW}o^l<%0UZ1hhua{avr!{UJ z?fEq^3>xbd;Efku&Y~hb@>Ax^>^&zA*@AAMkRWQY)s8lK_}$J#Ly6fI9NUGpCw|UC zP?7BR%MN$VlC17o&$BFNMOX4Cj&`B1RpaPac;oM=?{Ra%%XvM;MFbf1!m-x-DBar5 zb5`jroW-2*4GH>tlu*;zr#0D0#3pswT?P*I-p;N-)bkwbI_101{{ZJUz#aA-;1Xiz zuJ%!G9=u{NFl!M|zXSv!s}(IihHf4L91uo&OOk2no^b_#71 zrU&TXkX@W2%*#LJ(^3^?KhYqOL5MwLXT5!b>;QGRTBem?sQTBtc$OJ&W!I9nb6T`O zSd+D(ng^M-B)GK$@yX5A?-1_3T$B*#r4Q>Zp6?~W^_K&x;mnI1RrrsvPDOri>Th^t zteB`oDUXm1Ju6QzeWVNibI1C{jtkmf5ZqW?~{lGEC#H*t>;dOGVXH_?S^v zQq9Fkemlp+6L;C3CCJz};M?Ac@Ql2RnzN;2Lso>|S4ZT#ZHHC2q3Lle;!<65fRnN& z=gvTdzXnZq7HO$2rO`*bN3g(hvgoLkyB7c;T%%2EERC^uhd9n5+Dr(6JnWstL_HX&&twAz8^zm9x5*pI6bj zRZ7k_Gjyyp6L*?MsxixpbT8T7Hiy%Lu+i6g__;|Nj#9MF?RC?d2${()Qk*IiO^3;p zq1p6Zs>uWJ%r2wuqvQAR@F2F&ps4&NmP~X*{F~V+yW?GTS!JzZ3~|Ku{N1}oL&@dcqj|y8RrpRRTCV?4xDjbu{sA6Ay(<4 zy$UTwU+ts>KcRy`7&D*0ux}-s?}MS9on<;^AfpER0@_Rq?;D9o1M!ryllqX1o z9LDR|X>M^JHY4&8*s2A1D2fZtr`D)+5wCmFIFqPLV#cD?Xh^AjWp-Ve!uw4qta}rD z+j&uo+i>3zIDXy8KXFZ6Z{kGw7&RhQ9Ct0rhV%&GN6R+JXCn#@uR65Th3n|fp}Q$O z?ucAYW=^k6lRl)NE(o$p%*sGcaj*>?!yqOv9~og9IoUuEP%BNi@kg1&2AaQ}Q6o^P zZsim#Pf@w?qR7P!^QpS-)snf6!gfM2CwvRGa$s<@X!P5Uy?nwWpGYH2gW*O)=+(FJJa0vPJOK=(`khL1 z@BW0gYEJ2FtXR;pU-3#vqWl91C0>arVtTMgzBk(C_>cX-n+<~8vTB#|gh|=hEvT$A zS&Ut6G8WEp=k8sSkoN=F~noIa5qk9u^ zAQV1nq5!^j_~Jcpn$c1Ig&i+BuEbT)0lw#A2DMtH=T(;p-F{B5p|VxJ_xuf#x1q91 z-vdS0y(LAC7+~Qe)m)T5^QS1j_msfL$|*11i>J@)EdUGKoW9=AV$E2QkmqsG%>6-0Rhmb&JcnCwrgkhlI#W?MPL+na>p7~= z=zhPXs;zK9e%Y8>SLTwtSv>Y!{`Mj#B3d=beIm1X zS5T$$hXzIFe7u^41)ar*@7XW_!hGtT)vmRW)>hN?%J<0J1QL~8JYZNgRjfI=F68i7 z`s=0d$-aMiVS`}o52>=a_jJe#AJ8$7F96Yub$*OLwxGKYfr{=c6uXsZ@SHTq=aPa3>(UuA}&Bb zKyUzzn(f`JtHtj<=C^H|IowSWxKnWTs4K|$Z{q+&z4v<@K>xd&&Ha%q0s_|pCO%Am zSUo^nOMIc{Ga>~HF(B}a;X-K~x^uw5_K}`#=a^zImNRS3h`Z4$dHRmIW8dP=3XKWv zuGsDjE3ZSbQWpdQr-v&d2tZ%>9zkHgQd0^Z1T0qgKaA1QLmN#lUAhYzMGX66$r+?w zWkRw-rc8zo;zNG3K|8?m%P~Ka3y;a>pO*2}jY|NdblvHFUb9r+>Jh`=z)PynF%X0lgnt#62r3aGjtJO;8m-Q2f zfeErsi!4j?SUO1yP>@mQWmxM1jL^N?+hbJ`zDzzqS==;L*=A>@yV*jMBZ^nP_e)NI z?_5fN!=h^ADTtcU6|)dj?z2ZK3FvgoaKh#ov5Pw1Mubm|P!2Brb~@C2f(m0NOtZbocugNSh!2>OURpXY9W>eFq0)M+bUkJ12d6 zV-^-y78V&BQyV&4Yf}a3|858Cp%eP}hoSIK=lpL*x_>PH4|W{&?M;8|o4Xs+GO_6D zTREE8*#Dt*vbMF-cQiKo|C$Cg*}X35hne_KC;o5Kpu+q=)5!c#uaph#jg77UW3~Sq z-S+>z?*Fw^o{Y&Nz$N@3tcYSD3-Ejen7e@vD$alysfX7%w%EAZO5h<%AS{DoT)qLq zIB;+pni^C9F|GhbHc1V|Br6*{CgF7*a%8KZn91*fW+o!C@tSDV@w9L?FfKqCv{eED zZur4X1RXMgEi%{NVR32!MjHsDI_E?PTyuD4N~*CTBB*dU2eJzWegp>a_zTc<5CSYq z1e#b-pr4VNlarrYoLQllT9JxK#1QSU!wx_P9e~cqK!E-uXs2bMYlZCx2kiw%)%xTZ z^4@RYnF#1wVY_ZYJ8nVQ6aoI6MAnMrb4){EyA(k?6hV6s5#Zt(%x${pPJrz%0_`nA z)&1-|L^tw&Mu=|2ZW#2vM+m#Ku3+7Xgl+)(b{d2MH?9)eT!U;Dwi!W$S&45UW}(av z26(fv=|I(h%0(CeW_~b$(E`B80f7Z9z)e;_0MrNG3%Y?5!UvH7-mJjb2bHr3z`y_i D1tV89 literal 0 HcmV?d00001 diff --git a/spec/fixtures/api/autoupdater/msix/MSIXDevCert.cer b/spec/fixtures/api/autoupdater/msix/MSIXDevCert.cer new file mode 100644 index 0000000000000000000000000000000000000000..6a781185eeb8904de9ae211be6f96c58400b5234 GIT binary patch literal 774 zcmXqLVrDXEVtloLnTe5!NkDHY>xo0`SrgrV72LK>J)LL3%f_kI=F#?@mywa1mBB#5 zP|QGtjX9KsnTOXkCpEdGC_hiZH`p`6KtY_>$kf2x*wDZLh)tp-_>ByWOdtX#h6ct_ zc=R?gDj{3M$jZRn#K_M8bQc#>6C)$TL7nBFEhKraWm~Sep8eFPxpPOw^8O?7m)%zQ z2ZeqNef953?!1Q`=|vx|By3!Ky6V28ZsTWFwz`0{vlAW%5>xfy$Hr zb(ov~nC-Ryv(c^HMvju=y7h6w^Ib2`)r>y=xOr~R)f3S# zuN${c6yjXBBTmo#UD4Vy#e>T>tgdFc>z>qXd9bEMc*ZIJez9`#33L9$y*OTJ%~QRT zefuW8-q{jqzv@6h_A}RW5C76q0Pp~%F52nY#<8~(a_#?QSWh49Z zL*IG$P6<4iF@04+#`}eF_Zv=~&fjzJ*QcZF#CcVH`i*1{FYB@4d^S71yKXw4ig4O8 zozL~m6EG%8KGFO&=DZk=DUXRF4v&f_|3c_d*Zz_uSw*}nC)WZY1BW!fA+)< a^#eP1zHOJzxOk!RQ�ft_M~7g9HIM`Zopu literal 0 HcmV?d00001 diff --git a/spec/fixtures/api/autoupdater/msix/install_test_cert.ps1 b/spec/fixtures/api/autoupdater/msix/install_test_cert.ps1 new file mode 100644 index 0000000000..548b0727ae --- /dev/null +++ b/spec/fixtures/api/autoupdater/msix/install_test_cert.ps1 @@ -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" +} \ No newline at end of file diff --git a/spec/fixtures/api/autoupdater/msix/main.js b/spec/fixtures/api/autoupdater/msix/main.js new file mode 100644 index 0000000000..2c92ae6db3 --- /dev/null +++ b/spec/fixtures/api/autoupdater/msix/main.js @@ -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); + } +}); diff --git a/spec/lib/msix-helpers.ts b/spec/lib/msix-helpers.ts new file mode 100644 index 0000000000..027da53fc7 --- /dev/null +++ b/spec/lib/msix-helpers.ts @@ -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 { + 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 { + 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 { + 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 { + // 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 { + 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 { + 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 + }); + }); + }); +} From 64872a6cb8572f15af8a6ce43a6b6cb852195ec7 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Fri, 30 Jan 2026 10:07:16 +0100 Subject: [PATCH 17/38] fix: macOS menu item accelerators when item disabled (#49553) fix: macOS menu item acceerators when item disabled --- shell/browser/ui/cocoa/electron_menu_controller.mm | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/shell/browser/ui/cocoa/electron_menu_controller.mm b/shell/browser/ui/cocoa/electron_menu_controller.mm index 844ca12c31..94334af9c2 100644 --- a/shell/browser/ui/cocoa/electron_menu_controller.mm +++ b/shell/browser/ui/cocoa/electron_menu_controller.mm @@ -484,8 +484,11 @@ 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; } @@ -567,18 +570,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 From e033c1007562eb3b79ef827a7e797848dfe93e10 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Fri, 30 Jan 2026 16:53:04 +0100 Subject: [PATCH 18/38] fix: issues with xdg activation on Linux (#49499) --- shell/common/platform_util_linux.cc | 308 ++++++++++++++++------------ 1 file changed, 175 insertions(+), 133 deletions(-) diff --git a/shell/common/platform_util_linux.cc b/shell/common/platform_util_linux.cc index 267ae6ab03..38da09b6a6 100644 --- a/shell/common/platform_util_linux.cc +++ b/shell/common/platform_util_linux.cc @@ -7,7 +7,9 @@ #include #include +#include #include +#include #include #include @@ -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 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 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 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( + 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 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 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 bus_; - raw_ptr dbus_proxy_ = nullptr; - raw_ptr object_proxy_ = nullptr; - std::optional prefer_filemanager_interface_; + std::optional api_type_; + // The proxy objects are owned by `bus_`. + raw_ptr portal_object_proxy_ = nullptr; + raw_ptr file_manager_object_proxy_ = nullptr; + std::unique_ptr portal_open_directory_request_; + + // Requests that are queued until the API availability is determined. + std::queue pending_requests_; }; // Descriptions pulled from https://linux.die.net/man/1/xdg-open From 6825a522e9a535f421e102b4fce8463518429fd6 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Fri, 30 Jan 2026 19:23:43 +0100 Subject: [PATCH 19/38] fix: duplicate fullscreen macOS menu item (#49074) --- shell/browser/mac/electron_application_delegate.mm | 5 ----- shell/browser/ui/cocoa/electron_menu_controller.mm | 8 ++++++++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/shell/browser/mac/electron_application_delegate.mm b/shell/browser/mac/electron_application_delegate.mm index 72774bcd67..9f947f4933 100644 --- a/shell/browser/mac/electron_application_delegate.mm +++ b/shell/browser/mac/electron_application_delegate.mm @@ -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:) diff --git a/shell/browser/ui/cocoa/electron_menu_controller.mm b/shell/browser/ui/cocoa/electron_menu_controller.mm index 94334af9c2..7c58fb44f9 100644 --- a/shell/browser/ui/cocoa/electron_menu_controller.mm +++ b/shell/browser/ui/cocoa/electron_menu_controller.mm @@ -560,6 +560,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(); From 331d1e16f5b20cb453741af46842448383663a60 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Fri, 30 Jan 2026 20:16:39 +0100 Subject: [PATCH 20/38] refactor: address PathProvider TODO (#49563) --- filenames.gni | 1 + shell/app/electron_main_delegate.cc | 97 ----------------------- shell/common/electron_paths.cc | 117 ++++++++++++++++++++++++++++ shell/common/electron_paths.h | 4 + 4 files changed, 122 insertions(+), 97 deletions(-) create mode 100644 shell/common/electron_paths.cc diff --git a/filenames.gni b/filenames.gni index d8ca1fec8d..6b93b01b8e 100644 --- a/filenames.gni +++ b/filenames.gni @@ -592,6 +592,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", diff --git a/shell/app/electron_main_delegate.cc b/shell/app/electron_main_delegate.cc index 07d21ab529..278ae4c7be 100644 --- a/shell/app/electron_main_delegate.cc +++ b/shell/app/electron_main_delegate.cc @@ -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()) { diff --git a/shell/common/electron_paths.cc b/shell/common/electron_paths.cc new file mode 100644 index 0000000000..7fa29ed1e5 --- /dev/null +++ b/shell/common/electron_paths.cc @@ -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 diff --git a/shell/common/electron_paths.h b/shell/common/electron_paths.h index c614f99294..2bc0440419 100644 --- a/shell/common/electron_paths.h +++ b/shell/common/electron_paths.h @@ -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_ From f272723a33dcf1449527c56c8205ef1c6d4aa0bb Mon Sep 17 00:00:00 2001 From: axolotl <87679354+TheCommieAxolotl@users.noreply.github.com> Date: Sat, 31 Jan 2026 07:18:56 +1100 Subject: [PATCH 21/38] feat: Allow `View.setBounds` to animate (#48812) * feat: allow View::SetBounds to animate * fix: support width/height animations * fix: jumping on subsequent animations * fix: segfault race condition * fix: remove layer background * fix: layer clips not being reset * refactor: use gfx tween gin converter * fix: layer cleanups causing flickering views * chore: merge artifact * fix: missing private method in header * fix: return type * fix: do not set layer opacity * refactor: update animate parameter format * refactor: move animate into options object * chore: typo * docs: update * spec: add view animation test --- docs/api/view.md | 10 +- shell/browser/api/electron_api_view.cc | 133 ++++++++++++++++++++++++- shell/browser/api/electron_api_view.h | 3 +- spec/api-view-spec.ts | 13 +++ 4 files changed, 155 insertions(+), 4 deletions(-) diff --git a/docs/api/view.md b/docs/api/view.md index a3388a92e0..6dddd43c89 100644 --- a/docs/api/view.md +++ b/docs/api/view.md @@ -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()` diff --git a/shell/browser/api/electron_api_view.cc b/shell/browser/api/electron_api_view.cc index 986ec06c84..37a0c54ce2 100644 --- a/shell/browser/api/electron_api_view.cc +++ b/shell/browser/api/electron_api_view.cc @@ -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 { .Build(); } }; + +template <> +struct Converter { + static bool FromV8(v8::Isolate* isolate, + v8::Local 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 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 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 { diff --git a/shell/browser/api/electron_api_view.h b/shell/browser/api/electron_api_view.h index 2d1318e134..c05cbe3146 100644 --- a/shell/browser/api/electron_api_view.h +++ b/shell/browser/api/electron_api_view.h @@ -37,7 +37,7 @@ class View : public gin_helper::EventEmitter, std::optional index); void RemoveChildView(gin_helper::Handle 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 value); std::vector> GetChildren(); @@ -70,6 +70,7 @@ class View : public gin_helper::EventEmitter, void OnChildViewRemoved(views::View* observed_view, views::View* child) override; + ui::Layer* GetLayer(); void ApplyBorderRadius(); void ReorderChildView(gin_helper::Handle child, size_t index); diff --git a/spec/api-view-spec.ts b/spec/api-view-spec.ts index fc39bccb29..dc3deaf53f 100644 --- a/spec/api-view-spec.ts +++ b/spec/api-view-spec.ts @@ -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); + }); }); }); From 0cbf3c29185105179a1c91fb11dd7590f3d2ba15 Mon Sep 17 00:00:00 2001 From: Prachi Maskar Date: Sat, 31 Jan 2026 01:54:08 +0530 Subject: [PATCH 22/38] docs: add jsign instructions for Azure Trusted Signing on Linux/macOS (#49345) * docs: add jsign instructions for Azure Trusted Signing on Linux/macOS * docs: add clickable jsign link for Azure Trusted Signing --- docs/tutorial/code-signing.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/tutorial/code-signing.md b/docs/tutorial/code-signing.md index a51db39acf..95fd04250d 100644 --- a/docs/tutorial/code-signing.md +++ b/docs/tutorial/code-signing.md @@ -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 +``` + #### Using Electron Forge Electron Forge is the recommended way to sign your app as well as your `Squirrel.Windows` From 4b5d5f9dd5bb7eadec299060fc73ba1178388feb Mon Sep 17 00:00:00 2001 From: Keeley Hammond Date: Sun, 1 Feb 2026 22:27:42 -0800 Subject: [PATCH 23/38] fix: fix Windows MSIX release build errors (#49613) * fix: fix MSIX release build * fix: add C++/WinRT headers * build: modify include paths * fix: compile msix as seperate source set * build: add additional needed deps for msix --- BUILD.gn | 32 +++++++++++++++++++ filenames.gni | 2 -- .../browser/api/electron_api_msix_updater.cc | 15 +++++---- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/BUILD.gn b/BUILD.gn index 9dc7f22a66..781d80f718 100644 --- a/BUILD.gn +++ b/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", diff --git a/filenames.gni b/filenames.gni index 6b93b01b8e..3c351e260a 100644 --- a/filenames.gni +++ b/filenames.gni @@ -279,8 +279,6 @@ filenames = { "shell/browser/api/electron_api_in_app_purchase.h", "shell/browser/api/electron_api_menu.cc", "shell/browser/api/electron_api_menu.h", - "shell/browser/api/electron_api_msix_updater.cc", - "shell/browser/api/electron_api_msix_updater.h", "shell/browser/api/electron_api_native_theme.cc", "shell/browser/api/electron_api_native_theme.h", "shell/browser/api/electron_api_net_log.cc", diff --git a/shell/browser/api/electron_api_msix_updater.cc b/shell/browser/api/electron_api_msix_updater.cc index 605e7a45d0..21ed3fc33e 100644 --- a/shell/browser/api/electron_api_msix_updater.cc +++ b/shell/browser/api/electron_api_msix_updater.cc @@ -33,13 +33,14 @@ #include #include #include -#include -#include -#include -#include -#include -#include -#include +// 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 From 1159de52e7f12145e47dd8a0bb34e4e53a58662c Mon Sep 17 00:00:00 2001 From: Calvin Date: Mon, 2 Feb 2026 01:00:05 -0800 Subject: [PATCH 24/38] docs: `app.getGPUInfo()` may reject (#49580) --- docs/api/app.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/api/app.md b/docs/api/app.md index 8e7548a6e3..3d5a6f572b 100644 --- a/docs/api/app.md +++ b/docs/api/app.md @@ -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. From db5606b4b357aa6489cd9d1239b12fdf60888afc Mon Sep 17 00:00:00 2001 From: Robo Date: Mon, 2 Feb 2026 22:20:19 +0900 Subject: [PATCH 25/38] fix: handle out of order recording errors in skia graphite (#49608) --- patches/chromium/.patches | 1 + ...handle_out_of_order_recording_errors.patch | 348 ++++++++++++++++++ 2 files changed, 349 insertions(+) create mode 100644 patches/chromium/graphite_handle_out_of_order_recording_errors.patch diff --git a/patches/chromium/.patches b/patches/chromium/.patches index 6afa30b8a3..76c604d6ca 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -144,3 +144,4 @@ fix_linux_tray_id.patch expose_gtk_ui_platform_field.patch fix_os_crypt_async_cookie_encryption.patch patch_osr_control_screen_info.patch +graphite_handle_out_of_order_recording_errors.patch diff --git a/patches/chromium/graphite_handle_out_of_order_recording_errors.patch b/patches/chromium/graphite_handle_out_of_order_recording_errors.patch new file mode 100644 index 0000000000..64781f0c81 --- /dev/null +++ b/patches/chromium/graphite_handle_out_of_order_recording_errors.patch @@ -0,0 +1,348 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Sunny Sachanandani +Date: Fri, 30 Jan 2026 12:51:05 -0800 +Subject: [graphite] Handle out of order recording errors + +Explicitly handle out of order recording errors to crash just like async +shader compile failed errors. Also, emit the insert status to an UMA +histogram at 1% subsampling and log all error statuses at runtime. + +Bug: 458722690 +Change-Id: Id94657be6ae870dcf8adba71aff216386a6a6964 +Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7533855 +Commit-Queue: Sunny Sachanandani +Auto-Submit: Sunny Sachanandani +Reviewed-by: Jonathan Ross +Reviewed-by: Michael Ludwig +Cr-Commit-Position: refs/heads/main@{#1577487} + +diff --git a/gpu/command_buffer/service/graphite_shared_context.cc b/gpu/command_buffer/service/graphite_shared_context.cc +index f9a47ac16bddc2352d81f43629d8ce8c2d2b3b99..9856d68f893e4329fda5d002835613161b346bc1 100644 +--- a/gpu/command_buffer/service/graphite_shared_context.cc ++++ b/gpu/command_buffer/service/graphite_shared_context.cc +@@ -4,7 +4,10 @@ + + #include "gpu/command_buffer/service/graphite_shared_context.h" + ++#include "base/logging.h" + #include "base/memory/ptr_util.h" ++#include "base/metrics/histogram_macros.h" ++#include "base/rand_util.h" + #include "base/task/single_thread_task_runner.h" + #include "gpu/command_buffer/common/shm_count.h" + #include "third_party/skia/include/core/SkColorSpace.h" +@@ -14,6 +17,44 @@ + namespace gpu { + + namespace { ++// This is emitted to UMA - values should not be reordered, only appended! ++// LINT.IfChange(InsertRecordingStatusUma) ++enum class InsertRecordingStatusUma { ++ kSuccess, ++ kInvalidRecording, ++ kPromiseImageInstantiationFailed, ++ kAddCommandsFailed, ++ kAsyncShaderCompilesFailed, ++ kOutOfOrderRecording, ++ kMaxValue = kOutOfOrderRecording ++}; ++// LINT.ThenChange(//tools/metrics/histograms/metadata/gpu/enums.xml:GraphiteInsertRecordingStatus) ++ ++InsertRecordingStatusUma InsertRecordingStatusUma( ++ skgpu::graphite::InsertStatus insert_status) { ++ // InsertStatus almost behaves like an enum class, but not quite since it can ++ // convert to both bool and integer types and can't be used in a switch. ++ if (insert_status == skgpu::graphite::InsertStatus::kSuccess) { ++ return InsertRecordingStatusUma::kSuccess; ++ } else if (insert_status == ++ skgpu::graphite::InsertStatus::kInvalidRecording) { ++ return InsertRecordingStatusUma::kInvalidRecording; ++ } else if (insert_status == ++ skgpu::graphite::InsertStatus::kPromiseImageInstantiationFailed) { ++ return InsertRecordingStatusUma::kPromiseImageInstantiationFailed; ++ } else if (insert_status == ++ skgpu::graphite::InsertStatus::kAddCommandsFailed) { ++ return InsertRecordingStatusUma::kAddCommandsFailed; ++ } else if (insert_status == ++ skgpu::graphite::InsertStatus::kAsyncShaderCompilesFailed) { ++ return InsertRecordingStatusUma::kAsyncShaderCompilesFailed; ++ } else if (insert_status == ++ skgpu::graphite::InsertStatus::kOutOfOrderRecording) { ++ return InsertRecordingStatusUma::kOutOfOrderRecording; ++ } ++ NOTREACHED(); ++} ++ + struct RecordingContext { + skgpu::graphite::GpuFinishedProc old_finished_proc; + skgpu::graphite::GpuFinishedContext old_context; +@@ -216,20 +257,38 @@ bool GraphiteSharedContext::InsertRecordingImpl( + + auto insert_status = graphite_context_->insertRecording(*info_ptr); + +- // TODO(433845560): Check the kAddCommandsFailed failures. +- // Crash only if we're not simulating a failure for testing. + const bool simulating_insert_failure = + info_ptr->fSimulatedStatus != skgpu::graphite::InsertStatus::kSuccess; + +- // InsertStatus::kAsyncShaderCompilesFailed is also an unrecoverable error for +- // which we should also clear the disk shader cache in case the error was due +- // to a corrupted cached shader blob. ++ // Crash, log, or emit UMA only if we're not simulating a failure for testing. ++ if (!simulating_insert_failure) { ++ if (base::ShouldRecordSubsampledMetric(0.01)) { ++ UMA_HISTOGRAM_ENUMERATION("GPU.Graphite.InsertRecordingStatus", ++ InsertRecordingStatusUma(insert_status)); ++ } ++ if (insert_status != skgpu::graphite::InsertStatus::kSuccess) { ++ // skgpu::graphite::InsertStatus almost behaves like an enum class, but ++ // not quite - it can't be static_cast to an int. ++ LOG(ERROR) << "Graphite insertRecording failed with status " ++ << static_cast(InsertRecordingStatusUma(insert_status)); ++ } ++ } ++ ++ // kAsyncShaderCompilesFailed and kOutOfOrderRecording are unrecoverable ++ // failures because they cause future recordings to be rendered incorrectly. ++ // TODO(433845560): Check the kAddCommandsFailed failures. ++ const bool is_unrecoverable_failure = ++ insert_status == ++ skgpu::graphite::InsertStatus::kAsyncShaderCompilesFailed || ++ insert_status == skgpu::graphite::InsertStatus::kOutOfOrderRecording; ++ // For kAsyncShaderCompilesFailed, we should also clear the disk shader ++ // cache in case the error was due to a corrupted cached shader blob. ++ std::optional use_shader_cache; + if (insert_status == + skgpu::graphite::InsertStatus::kAsyncShaderCompilesFailed) { +- GpuProcessShmCount::ScopedIncrement use_shader_cache( +- use_shader_cache_shm_count_); +- CHECK(simulating_insert_failure); ++ use_shader_cache.emplace(use_shader_cache_shm_count_); + } ++ CHECK(simulating_insert_failure || !is_unrecoverable_failure); + + // All other failure modes are recoverable in the sense that future recordings + // will be rendered correctly, so merely return a boolean here so that callers +diff --git a/gpu/command_buffer/service/graphite_shared_context_unittest.cc b/gpu/command_buffer/service/graphite_shared_context_unittest.cc +index 0de107a9d86ff8217a4c537598f198f313ecf9df..852343ab6804efed962b707b748ddf1920c20ea7 100644 +--- a/gpu/command_buffer/service/graphite_shared_context_unittest.cc ++++ b/gpu/command_buffer/service/graphite_shared_context_unittest.cc +@@ -6,6 +6,7 @@ + + #include "base/threading/thread.h" + #include "gpu/command_buffer/common/shm_count.h" ++#include "gpu/command_buffer/service/skia_utils.h" + #include "skia/buildflags.h" + #include "testing/gmock/include/gmock/gmock.h" + #include "testing/gtest/include/gtest/gtest.h" +@@ -86,8 +87,7 @@ class GraphiteSharedContextTest : public testing::TestWithParam { + + wgpu::DeviceDescriptor device_desc = {}; + +- wgpu::Device device = +- wgpu::Adapter(adapters[0].Get()).CreateDevice(&device_desc); ++ auto device = wgpu::Adapter(adapters[0].Get()).CreateDevice(&device_desc); + CHECK(device); + + skgpu::graphite::DawnBackendContext backend_context = {}; +@@ -95,7 +95,11 @@ class GraphiteSharedContextTest : public testing::TestWithParam { + backend_context.fDevice = device; + backend_context.fQueue = device.GetQueue(); + +- skgpu::graphite::ContextOptions context_options = {}; ++ // Use the default Graphite context options that Chromium uses e.g. disallow ++ // things like out of order recordings. ++ gpu::GpuDriverBugWorkarounds workarounds; ++ auto context_options = GetDefaultGraphiteContextOptions(workarounds); ++ + graphite_shared_context_ = std::make_unique( + skgpu::graphite::ContextFactory::MakeDawn(backend_context, + context_options), +@@ -133,14 +137,12 @@ TEST_P(GraphiteSharedContextTest, ConcurrentAccess) { + auto run_graphite_functions = + [](GraphiteSharedContext* graphite_shared_context) { + // Call a method that acquires the lock +- std::unique_ptr recorder = +- graphite_shared_context->makeRecorder(); ++ auto recorder = graphite_shared_context->makeRecorder(); + EXPECT_TRUE(recorder); + + for (int i = 0; i < 2; ++i) { + for (int j = 0; j < 10; ++j) { +- std::unique_ptr recording = +- recorder->snap(); ++ auto recording = recorder->snap(); + skgpu::graphite::InsertRecordingInfo info = {}; + info.fRecording = recording.get(); + EXPECT_TRUE(recording); +@@ -172,18 +174,17 @@ TEST_P(GraphiteSharedContextTest, ConcurrentAccess) { + } + + TEST_P(GraphiteSharedContextTest, AsyncShaderCompilesFailed) { +- std::unique_ptr recorder = +- graphite_shared_context_->makeRecorder(); ++ auto recorder = graphite_shared_context_->makeRecorder(); + EXPECT_TRUE(recorder); + + auto ii = SkImageInfo::Make(64, 64, kN32_SkColorType, kPremul_SkAlphaType); +- sk_sp surface1 = SkSurfaces::RenderTarget(recorder.get(), ii); ++ auto surface1 = SkSurfaces::RenderTarget(recorder.get(), ii); + surface1->getCanvas()->clear(SK_ColorRED); + +- sk_sp surface2 = SkSurfaces::RenderTarget(recorder.get(), ii); ++ auto surface2 = SkSurfaces::RenderTarget(recorder.get(), ii); + surface2->getCanvas()->drawImage(surface1->makeTemporaryImage(), 0, 0); + +- std::unique_ptr recording = recorder->snap(); ++ auto recording = recorder->snap(); + EXPECT_TRUE(recording); + + skgpu::graphite::InsertRecordingInfo info = {}; +@@ -197,19 +198,43 @@ TEST_P(GraphiteSharedContextTest, AsyncShaderCompilesFailed) { + EXPECT_FALSE(graphite_shared_context_->insertRecording(info)); + } + ++TEST_P(GraphiteSharedContextTest, OutOfOrderRecording) { ++ auto recorder = graphite_shared_context_->makeRecorder(); ++ EXPECT_TRUE(recorder); ++ ++ auto ii = SkImageInfo::Make(64, 64, kN32_SkColorType, kPremul_SkAlphaType); ++ auto surface1 = SkSurfaces::RenderTarget(recorder.get(), ii); ++ surface1->getCanvas()->clear(SK_ColorRED); ++ auto recording1 = recorder->snap(); ++ EXPECT_TRUE(recording1); ++ ++ auto surface2 = SkSurfaces::RenderTarget(recorder.get(), ii); ++ surface2->getCanvas()->drawImage(surface1->makeTemporaryImage(), 0, 0); ++ auto recording2 = recorder->snap(); ++ EXPECT_TRUE(recording2); ++ ++ skgpu::graphite::InsertRecordingInfo info = {}; ++ ++ info.fRecording = recording2.get(); ++ graphite_shared_context_->insertRecording(info); ++ ++ info.fRecording = recording1.get(); ++ EXPECT_DEATH_IF_SUPPORTED(graphite_shared_context_->insertRecording(info), ++ ""); ++} ++ + TEST_P(GraphiteSharedContextTest, AddCommandsFailed) { +- std::unique_ptr recorder = +- graphite_shared_context_->makeRecorder(); ++ auto recorder = graphite_shared_context_->makeRecorder(); + EXPECT_TRUE(recorder); + + auto ii = SkImageInfo::Make(64, 64, kN32_SkColorType, kPremul_SkAlphaType); +- sk_sp surface1 = SkSurfaces::RenderTarget(recorder.get(), ii); ++ auto surface1 = SkSurfaces::RenderTarget(recorder.get(), ii); + surface1->getCanvas()->clear(SK_ColorRED); + +- sk_sp surface2 = SkSurfaces::RenderTarget(recorder.get(), ii); ++ auto surface2 = SkSurfaces::RenderTarget(recorder.get(), ii); + surface2->getCanvas()->drawImage(surface1->makeTemporaryImage(), 0, 0); + +- std::unique_ptr recording = recorder->snap(); ++ auto recording = recorder->snap(); + EXPECT_TRUE(recording); + + skgpu::graphite::InsertRecordingInfo info = {}; +@@ -223,39 +248,37 @@ TEST_P(GraphiteSharedContextTest, AddCommandsFailed) { + } + + TEST_P(GraphiteSharedContextTest, LowPendingRecordings) { +- std::unique_ptr recorder = +- graphite_shared_context_->makeRecorder(); ++ auto recorder = graphite_shared_context_->makeRecorder(); + EXPECT_TRUE(recorder); + +- std::unique_ptr recording = recorder->snap(); +- EXPECT_TRUE(recording); +- +- skgpu::graphite::InsertRecordingInfo info = {}; +- info.fRecording = recording.get(); +- + // No flush is expected if the number of pending recordings is low. + EXPECT_CALL(backend_flush_callback_, Flush()).Times(0); + + for (size_t i = 0; i < kMaxPendingRecordings - 1; ++i) { ++ auto recording = recorder->snap(); ++ EXPECT_TRUE(recording); ++ ++ skgpu::graphite::InsertRecordingInfo info = {}; ++ info.fRecording = recording.get(); ++ + EXPECT_TRUE(graphite_shared_context_->insertRecording(info)); + } + } + + TEST_P(GraphiteSharedContextTest, MaxPendingRecordings) { +- std::unique_ptr recorder = +- graphite_shared_context_->makeRecorder(); ++ auto recorder = graphite_shared_context_->makeRecorder(); + EXPECT_TRUE(recorder); + +- std::unique_ptr recording = recorder->snap(); +- EXPECT_TRUE(recording); +- +- skgpu::graphite::InsertRecordingInfo info = {}; +- info.fRecording = recording.get(); +- + // Expect a flush when the number of pending recordings reaches the max. + EXPECT_CALL(backend_flush_callback_, Flush()).Times(1); + + for (size_t i = 0; i < kMaxPendingRecordings; ++i) { ++ auto recording = recorder->snap(); ++ EXPECT_TRUE(recording); ++ ++ skgpu::graphite::InsertRecordingInfo info = {}; ++ info.fRecording = recording.get(); ++ + EXPECT_TRUE(graphite_shared_context_->insertRecording(info)); + } + } +diff --git a/tools/metrics/histograms/metadata/gpu/enums.xml b/tools/metrics/histograms/metadata/gpu/enums.xml +index 2df046e16f9186c340042506eb23a2526e515528..106bbdf71a3a067a7378486caa8423a839465455 100644 +--- a/tools/metrics/histograms/metadata/gpu/enums.xml ++++ b/tools/metrics/histograms/metadata/gpu/enums.xml +@@ -1112,6 +1112,19 @@ Called by update_gpu_driver_bug_workaround_entries.py.--> + + + ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ + + + +diff --git a/tools/metrics/histograms/metadata/gpu/histograms.xml b/tools/metrics/histograms/metadata/gpu/histograms.xml +index 6b28a436fb4be0ba088de243f54134b4b024bfd4..f67c31921c224dcd9e94f07d60f32ec16debaaac 100644 +--- a/tools/metrics/histograms/metadata/gpu/histograms.xml ++++ b/tools/metrics/histograms/metadata/gpu/histograms.xml +@@ -1033,6 +1033,16 @@ chromium-metrics-reviews@google.com. + + + ++ ++ sunnyps@chromium.org ++ michaelludwig@google.com ++ chrome-gpu-metric-alerts@chromium.org ++

++ The return value for each Graphite insertRecording call made by Chromium. ++ ++ ++ + + vasilyt@chromium.org From 3776731f4a7e9e5f3094133649fa8b4a0723b0b7 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Mon, 2 Feb 2026 23:01:34 +0100 Subject: [PATCH 26/38] fix: menu state in macOS dock menus (#49574) --- shell/browser/mac/electron_application_delegate.mm | 9 ++++++++- shell/browser/ui/cocoa/electron_menu_controller.h | 4 ++++ shell/browser/ui/cocoa/electron_menu_controller.mm | 2 -- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/shell/browser/mac/electron_application_delegate.mm b/shell/browser/mac/electron_application_delegate.mm index 9f947f4933..7ab2741aa1 100644 --- a/shell/browser/mac/electron_application_delegate.mm +++ b/shell/browser/mac/electron_application_delegate.mm @@ -109,7 +109,14 @@ static NSDictionary* UNNotificationResponseToNSDictionary( } - (NSMenu*)applicationDockMenu:(NSApplication*)sender { - return menu_controller_ ? menu_controller_.menu : nil; + if (!menu_controller_) + return nil; + + // Manually refresh menu state since menuWillOpen: is not called + // by macOS for dock menus for some reason before they are displayed. + NSMenu* menu = menu_controller_.menu; + [menu_controller_ refreshMenuTree:menu]; + return menu; } - (BOOL)application:(NSApplication*)sender openFile:(NSString*)filename { diff --git a/shell/browser/ui/cocoa/electron_menu_controller.h b/shell/browser/ui/cocoa/electron_menu_controller.h index 489f42ad77..b0f104215d 100644 --- a/shell/browser/ui/cocoa/electron_menu_controller.h +++ b/shell/browser/ui/cocoa/electron_menu_controller.h @@ -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; diff --git a/shell/browser/ui/cocoa/electron_menu_controller.mm b/shell/browser/ui/cocoa/electron_menu_controller.mm index 7c58fb44f9..81bfad9463 100644 --- a/shell/browser/ui/cocoa/electron_menu_controller.mm +++ b/shell/browser/ui/cocoa/electron_menu_controller.mm @@ -493,8 +493,6 @@ NSArray* ConvertSharingItemToNS(const SharingItem& item) { : NSControlStateValueOff; } -// Recursively refreshes the menu tree starting from |menu|, applying the -// model state to each menu item. - (void)refreshMenuTree:(NSMenu*)menu { for (NSMenuItem* item in menu.itemArray) { [self applyStateToMenuItem:item]; From 4413a0f642af2f8aab04e27939b74f78edba4056 Mon Sep 17 00:00:00 2001 From: "Mr.Chaofan" Date: Tue, 3 Feb 2026 01:19:35 -0800 Subject: [PATCH 27/38] fix: wrong cause and removed flag in cookie change listener (#49103) --- docs/api/cookies.md | 7 ++++++- docs/breaking-changes.md | 8 ++++++++ shell/browser/api/electron_api_cookies.cc | 20 ++++++++++++++++++-- spec/api-session-spec.ts | 2 +- 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/docs/api/cookies.md b/docs/api/cookies.md index 8a3576d3fc..e2bb62a14b 100644 --- a/docs/api/cookies.md +++ b/docs/api/cookies.md @@ -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. diff --git a/docs/breaking-changes.md b/docs/breaking-changes.md index 6e962d4c39..3a775130c6 100644 --- a/docs/breaking-changes.md +++ b/docs/breaking-changes.md @@ -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 diff --git a/shell/browser/api/electron_api_cookies.cc b/shell/browser/api/electron_api_cookies.cc index c9ef35106e..417ffa217b 100644 --- a/shell/browser/api/electron_api_cookies.cc +++ b/shell/browser/api/electron_api_cookies.cc @@ -80,6 +80,11 @@ struct Converter { 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: @@ -272,6 +277,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}; @@ -435,10 +451,10 @@ v8::Local 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 diff --git a/spec/api-session-spec.ts b/spec/api-session-spec.ts index 0ce49400f4..a4a812214e 100644 --- a/spec/api-session-spec.ts +++ b/spec/api-session-spec.ts @@ -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); From bdf2b674622fac64112f4ea7299120ab5a332dd0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 15:03:22 +0100 Subject: [PATCH 28/38] build(deps): bump github/codeql-action from 4.32.0 to 4.32.1 (#49629) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.32.0 to 4.32.1. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/b20883b0cd1f46c72ae0ba6d1090936928f9fa30...6bc82e05fd0ea64601dd4b465378bbcf57de0314) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.32.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/scorecards.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 4cafc83c57..b15a998a9b 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -51,6 +51,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v3.29.5 + uses: github/codeql-action/upload-sarif@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # v3.29.5 with: sarif_file: results.sarif From 86209f60eb3aedf8368fff0d415a17d2dcb7d0a6 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Tue, 3 Feb 2026 15:15:54 +0100 Subject: [PATCH 29/38] fix: possible crash in FileSystem API (#49578) Refs https://chromium-review.googlesource.com/6880247 Fixes a crash that can arise in the File System Access API in the following scenario: 1. Create fileHandle1 at path1. 2. Call fileHandle1.remove() or user manually delete the file. 3. Create fileHandle2 at path2. 4. fileHandle2.move(path1). --- .../file_system_access_permission_context.cc | 47 ++++++++++++++----- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/shell/browser/file_system_access/file_system_access_permission_context.cc b/shell/browser/file_system_access/file_system_access_permission_context.cc index 2dcbfcb95c..0ae068d221 100644 --- a/shell/browser/file_system_access/file_system_access_permission_context.cc +++ b/shell/browser/file_system_access/file_system_access_permission_context.cc @@ -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& 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); } } From 233caf84695d56b6ffad83461e2728fe7588cddb Mon Sep 17 00:00:00 2001 From: loc Date: Tue, 3 Feb 2026 08:04:06 -0800 Subject: [PATCH 30/38] fix(squirrel.mac): clean up old staged updates before downloading new update (#49365) fix: clean up old staged updates before downloading new update When checkForUpdates() is called while an update is already staged, Squirrel creates a new temporary directory for the download without cleaning up the old one. This can lead to disk usage growth when new versions are released while the app hasn't restarted. This adds a force parameter to pruneUpdateDirectories that bypasses the AwaitingRelaunch state check. This is called before creating a new temp directory, ensuring old staged updates are cleaned up. --- patches/squirrel.mac/.patches | 1 + ...pdates_before_downloading_new_update.patch | 64 +++++++++++++ spec/api-autoupdater-darwin-spec.ts | 94 +++++++++++++++++++ 3 files changed, 159 insertions(+) create mode 100644 patches/squirrel.mac/fix_clean_up_old_staged_updates_before_downloading_new_update.patch diff --git a/patches/squirrel.mac/.patches b/patches/squirrel.mac/.patches index 0893372102..ea7939dcfd 100644 --- a/patches/squirrel.mac/.patches +++ b/patches/squirrel.mac/.patches @@ -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 diff --git a/patches/squirrel.mac/fix_clean_up_old_staged_updates_before_downloading_new_update.patch b/patches/squirrel.mac/fix_clean_up_old_staged_updates_before_downloading_new_update.patch new file mode 100644 index 0000000000..3114490a15 --- /dev/null +++ b/patches/squirrel.mac/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 +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]; diff --git a/spec/api-autoupdater-darwin-spec.ts b/spec/api-autoupdater-darwin-spec.ts index 24709ef21b..9fad606898 100644 --- a/spec/api-autoupdater-darwin-spec.ts +++ b/spec/api-autoupdater-darwin-spec.ts @@ -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 = {}; 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((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', From c3428fa4133f66d0da336849a71667fb05fe64b8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 11:05:40 -0500 Subject: [PATCH 31/38] build(deps): bump lodash from 4.17.21 to 4.17.23 in the npm_and_yarn group across 1 directory (#49577) build(deps): bump lodash in the npm_and_yarn group across 1 directory Bumps the npm_and_yarn group with 1 update in the / directory: [lodash](https://github.com/lodash/lodash). Updates `lodash` from 4.17.21 to 4.17.23 - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23) --- updated-dependencies: - dependency-name: lodash dependency-version: 4.17.23 dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index b1ab7a902c..447278be9f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6875,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 @@ -8685,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 @@ -9660,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: From 8fdb7549bb3009db3f1c42e1d167028500aba39e Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Tue, 3 Feb 2026 17:34:58 +0100 Subject: [PATCH 32/38] fix: alt-space should route through 'system-context-menu' (#49619) fix: alt-space should route through system-context-menu --- .../electron_desktop_window_tree_host_win.cc | 25 +++++++++++++++++++ .../electron_desktop_window_tree_host_win.h | 4 +++ 2 files changed, 29 insertions(+) diff --git a/shell/browser/ui/win/electron_desktop_window_tree_host_win.cc b/shell/browser/ui/win/electron_desktop_window_tree_host_win.cc index 42039cd92f..5e5683a387 100644 --- a/shell/browser/ui/win/electron_desktop_window_tree_host_win.cc +++ b/shell/browser/ui/win/electron_desktop_window_tree_host_win.cc @@ -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); diff --git a/shell/browser/ui/win/electron_desktop_window_tree_host_win.h b/shell/browser/ui/win/electron_desktop_window_tree_host_win.h index 366dd08270..dbb0cbf5a4 100644 --- a/shell/browser/ui/win/electron_desktop_window_tree_host_win.h +++ b/shell/browser/ui/win/electron_desktop_window_tree_host_win.h @@ -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; From 28858ecb85336fc331e76b2f981dad0d2e7cbf27 Mon Sep 17 00:00:00 2001 From: Niklas Wenzel Date: Tue, 3 Feb 2026 17:39:07 +0100 Subject: [PATCH 33/38] chore: add "I have built and tested this PR" box to PR template (#49434) --- .github/PULL_REQUEST_TEMPLATE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 0d6aa8d39e..c4826abeac 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -11,6 +11,7 @@ Contributors guide: https://github.com/electron/electron/blob/main/CONTRIBUTING. - [ ] 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) From def7854848d7b2bed6cd96d8b31d2a5de91048ec Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Tue, 3 Feb 2026 17:57:14 +0100 Subject: [PATCH 34/38] ci: use new case syntax in workflows (#49590) ci: use new case synta in workflows --- .github/actions/build-electron/action.yml | 2 +- .github/workflows/pipeline-segment-electron-build.yml | 9 +++++++-- .../pipeline-segment-electron-clang-tidy.yml | 11 ++++++++--- .../workflows/pipeline-segment-electron-gn-check.yml | 7 ++++++- .github/workflows/pipeline-segment-electron-test.yml | 9 +++++++-- 5 files changed, 29 insertions(+), 9 deletions(-) diff --git a/.github/actions/build-electron/action.yml b/.github/actions/build-electron/action.yml index ba5bef4f3f..762eee223b 100644 --- a/.github/actions/build-electron/action.yml +++ b/.github/actions/build-electron/action.yml @@ -40,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 diff --git a/.github/workflows/pipeline-segment-electron-build.yml b/.github/workflows/pipeline-segment-electron-build.yml index a70ac7e9d5..1012a3d1a8 100644 --- a/.github/workflows/pipeline-segment-electron-build.yml +++ b/.github/workflows/pipeline-segment-electron-build.yml @@ -78,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 }} @@ -201,7 +206,7 @@ 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 }}' diff --git a/.github/workflows/pipeline-segment-electron-clang-tidy.yml b/.github/workflows/pipeline-segment-electron-clang-tidy.yml index 7881e619d3..27b2d79db9 100644 --- a/.github/workflows/pipeline-segment-electron-clang-tidy.yml +++ b/.github/workflows/pipeline-segment-electron-clang-tidy.yml @@ -28,7 +28,12 @@ concurrency: cancel-in-progress: true env: - 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: @@ -41,10 +46,10 @@ jobs: contents: read container: ${{ fromJSON(inputs.clang-tidy-container) }} env: - BUILD_TYPE: ${{ inputs.target-platform == 'macos' && 'darwin' || inputs.target-platform }} + BUILD_TYPE: ${{ case(inputs.target-platform == 'macos', 'darwin', inputs.target-platform) }} TARGET_ARCH: ${{ inputs.target-arch }} TARGET_PLATFORM: ${{ inputs.target-platform }} - ARTIFACT_KEY: ${{ inputs.target-platform == 'macos' && 'darwin' || inputs.target-platform }}_${{ inputs.target-arch }} + ARTIFACT_KEY: ${{ case(inputs.target-platform == 'macos', 'darwin', inputs.target-platform) }}_${{ inputs.target-arch }} steps: - name: Checkout Electron uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd diff --git a/.github/workflows/pipeline-segment-electron-gn-check.yml b/.github/workflows/pipeline-segment-electron-gn-check.yml index 0c28e2c8c1..9974edbaa9 100644 --- a/.github/workflows/pipeline-segment-electron-gn-check.yml +++ b/.github/workflows/pipeline-segment-electron-gn-check.yml @@ -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: diff --git a/.github/workflows/pipeline-segment-electron-test.yml b/.github/workflows/pipeline-segment-electron-test.yml index 68c46f3ef4..532b9f1f12 100644 --- a/.github/workflows/pipeline-segment-electron-test.yml +++ b/.github/workflows/pipeline-segment-electron-test.yml @@ -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 }} From 51a9101c3de7794baad9c35cce57adecf9ea3ad3 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Tue, 3 Feb 2026 18:02:06 +0100 Subject: [PATCH 35/38] build: remove Core Graphics private macOS APIs on MAS (#49633) --- ...dless_mode_handling_in_native_widget.patch | 4 +- ..._avoid_private_macos_api_usage.patch.patch | 80 ++++++++++++++++++- 2 files changed, 80 insertions(+), 4 deletions(-) diff --git a/patches/chromium/fix_adjust_headless_mode_handling_in_native_widget.patch b/patches/chromium/fix_adjust_headless_mode_handling_in_native_widget.patch index ce4341453a..df525f18d2 100644 --- a/patches/chromium/fix_adjust_headless_mode_handling_in_native_widget.patch +++ b/patches/chromium/fix_adjust_headless_mode_handling_in_native_widget.patch @@ -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 849da439a046aea133946572c79964858e4e7ba5..59200581b6e47a8c6c55d2197cd08ce001efdfcc 100644 +index b620a31aed5801160dc49d964df56410786e6f2e..6161185d8360822cf349114e530aa896f01af25c 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 -@@ -530,7 +530,7 @@ NSUInteger CountBridgedWindows(NSArray* child_windows) { +@@ -533,7 +533,7 @@ NSUInteger CountBridgedWindows(NSArray* child_windows) { is_translucent_window_ = params->is_translucent; pending_restoration_data_ = params->state_restoration_data; diff --git a/patches/chromium/mas_avoid_private_macos_api_usage.patch.patch b/patches/chromium/mas_avoid_private_macos_api_usage.patch.patch index fe7718fa94..106a23ca00 100644 --- a/patches/chromium/mas_avoid_private_macos_api_usage.patch.patch +++ b/patches/chromium/mas_avoid_private_macos_api_usage.patch.patch @@ -12,6 +12,9 @@ Subject: mas: avoid private macOS API usage * _LSSetApplicationLaunchServicesServerConnectionStatus * AreDeviceAndUserJoinedToDomain * _CFIsObjC + * CGSSetWindowCaptureExcludeShape + * CGRegionCreateWithRect + * CTFontCopyVariationAxesInternal * AudioDeviceDuck * NSNextStepFrame * NSThemeFrame @@ -663,7 +666,7 @@ index 3c7bfe98848cbb3a309737b84ff1c8d683a83ec7..84036a1c4635e7b5bca5ebe1ea5e303b - (NSWindow*)rootWindow { 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 414874d84338ff12e707d52bc82483957d74d8ef..849da439a046aea133946572c79964858e4e7ba5 100644 +index 414874d84338ff12e707d52bc82483957d74d8ef..b620a31aed5801160dc49d964df56410786e6f2e 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 @@ @@ -674,7 +677,21 @@ index 414874d84338ff12e707d52bc82483957d74d8ef..849da439a046aea133946572c7996485 #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" -@@ -731,10 +732,12 @@ NSUInteger CountBridgedWindows(NSArray* child_windows) { +@@ -73,10 +74,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); +@@ -731,10 +735,12 @@ NSUInteger CountBridgedWindows(NSArray* child_windows) { // this should be treated as an error and caught early. CHECK(bridged_view_); @@ -687,6 +704,25 @@ index 414874d84338ff12e707d52bc82483957d74d8ef..849da439a046aea133946572c7996485 // Beware: This view was briefly removed (in favor of a bare CALayer) in // https://crrev.com/c/1236675. The ordering of unassociated layers relative +@@ -1220,6 +1226,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; +@@ -1229,6 +1236,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 3c60e4e519f36a098704d744e3659ad0b8083157..123118d66734295f7b00e807aa0437ac76ab3f57 100644 --- a/components/viz/service/BUILD.gn @@ -1708,6 +1744,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 From 431f77ca1c265a347ce3cc5376aed129bf4f0f89 Mon Sep 17 00:00:00 2001 From: Keeley Hammond Date: Wed, 4 Feb 2026 07:11:00 -0800 Subject: [PATCH 36/38] fix: remove menu observer before destroying menu_controller_ (#49648) * fix: remove menu observer before destroying menu_controller_ * fix: resolves private inheritance conflict --- shell/browser/api/electron_api_menu.cc | 4 ++++ shell/browser/api/electron_api_menu.h | 4 ++++ shell/browser/api/electron_api_menu_mac.mm | 6 +++++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/shell/browser/api/electron_api_menu.cc b/shell/browser/api/electron_api_menu.cc index 269080dba7..95bf1a1e92 100644 --- a/shell/browser/api/electron_api_menu.cc +++ b/shell/browser/api/electron_api_menu.cc @@ -68,6 +68,10 @@ Menu::Menu(gin::Arguments* args) } Menu::~Menu() { + RemoveModelObserver(); +} + +void Menu::RemoveModelObserver() { if (model_) { model_->RemoveObserver(this); } diff --git a/shell/browser/api/electron_api_menu.h b/shell/browser/api/electron_api_menu.h index fc76a0c3df..1a06968233 100644 --- a/shell/browser/api/electron_api_menu.h +++ b/shell/browser/api/electron_api_menu.h @@ -61,6 +61,10 @@ class Menu : public gin::Wrappable, 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); diff --git a/shell/browser/api/electron_api_menu_mac.mm b/shell/browser/api/electron_api_menu_mac.mm index 0d260562fc..42d7f2778c 100644 --- a/shell/browser/api/electron_api_menu_mac.mm +++ b/shell/browser/api/electron_api_menu_mac.mm @@ -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 frame, From d7378d96a55fe55b6986f4aaad275d58c33e1e06 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Wed, 4 Feb 2026 16:13:11 +0100 Subject: [PATCH 37/38] docs: add Wayland note to `win.getPosition()` and `win.getBounds()` (#49632) docs: add Wayland note to win.getPosition() --- docs/api/base-window.md | 6 ++++++ docs/api/browser-window.md | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/docs/api/base-window.md b/docs/api/base-window.md index 43aa8a156b..bfc39dece9 100644 --- a/docs/api/base-window.md +++ b/docs/api/base-window.md @@ -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 diff --git a/docs/api/browser-window.md b/docs/api/browser-window.md index f04267bc61..99a00ce040 100644 --- a/docs/api/browser-window.md +++ b/docs/api/browser-window.md @@ -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 From ef11669b67c6b025207077c27b3beb04a72722a0 Mon Sep 17 00:00:00 2001 From: David Sanders Date: Wed, 4 Feb 2026 07:13:27 -0800 Subject: [PATCH 38/38] ci: handle PRs with no checks in rerun apply patches (#49630) --- .github/workflows/rerun-apply-patches.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/rerun-apply-patches.yml b/.github/workflows/rerun-apply-patches.yml index 61423b2bbc..87f24bc5fd 100644 --- a/.github/workflows/rerun-apply-patches.yml +++ b/.github/workflows/rerun-apply-patches.yml @@ -35,20 +35,20 @@ jobs: echo "Processing PR #${PR_NUMBER}" # Find the Apply Patches workflow check for this PR - CHECK=$(gh pr checks "$PR_NUMBER" --json link,name,state,workflow --jq '[.[] | select(.workflow == "Apply Patches" and .name == "apply-patches")] | first') + CHECK=$(gh pr view "$PR_NUMBER" --json statusCheckRollup --jq '[.statusCheckRollup[] | select(.workflowName == "Apply Patches" and .name == "apply-patches")] | first') if [ -z "$CHECK" ] || [ "$CHECK" = "null" ]; then echo " No Apply Patches workflow found for PR #${PR_NUMBER}" continue fi - STATE=$(echo "$CHECK" | jq -r '.state') - if [ "$STATE" = "SKIPPED" ]; then + CONCLUSION=$(echo "$CHECK" | jq -r '.conclusion') + if [ "$CONCLUSION" = "SKIPPED" ]; then echo " apply-patches job was skipped for PR #${PR_NUMBER} (no patches)" continue fi - LINK=$(echo "$CHECK" | jq -r '.link') + LINK=$(echo "$CHECK" | jq -r '.detailsUrl') # Extract the run ID from the link (format: .../runs/RUN_ID/job/JOB_ID) RUN_ID=$(echo "$LINK" | grep -oE 'runs/[0-9]+' | cut -d'/' -f2)