mirror of
https://github.com/electron/electron.git
synced 2026-04-10 03:01:51 -04:00
Compare commits
43 Commits
print-win
...
event-emit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5598bb7286 | ||
|
|
f79b2489b2 | ||
|
|
f4f2e7cb84 | ||
|
|
accd419b48 | ||
|
|
48df7eeff0 | ||
|
|
7b4d42f248 | ||
|
|
018cdfde54 | ||
|
|
5e75ee34d3 | ||
|
|
dd7c4dc207 | ||
|
|
a9a528c472 | ||
|
|
adfb387767 | ||
|
|
2868c69f4d | ||
|
|
4c1808eeea | ||
|
|
ef7a582e16 | ||
|
|
ec9efa0b86 | ||
|
|
07a2541b5a | ||
|
|
a5c0665bd8 | ||
|
|
8a5c5a4fe2 | ||
|
|
3ce98b60be | ||
|
|
774c5e52d7 | ||
|
|
0446e7d051 | ||
|
|
2290cf57c2 | ||
|
|
a839fb94aa | ||
|
|
2e2c56adde | ||
|
|
678adeaf7c | ||
|
|
1d14694dec | ||
|
|
a48f03fb8d | ||
|
|
f6b43cb0ef | ||
|
|
7451d560ba | ||
|
|
27edd6e21c | ||
|
|
ec3a18d438 | ||
|
|
02d4101ca3 | ||
|
|
fdaba4c6b0 | ||
|
|
542ff828ab | ||
|
|
4371a4dceb | ||
|
|
60f4b07723 | ||
|
|
f282bec8ef | ||
|
|
cef388de3d | ||
|
|
1828690467 | ||
|
|
f4c4cd14ac | ||
|
|
3db3996102 | ||
|
|
dbcf0fb5f0 | ||
|
|
29750dda08 |
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -19,6 +19,7 @@ DEPS @electron/wg-upgrades
|
||||
/lib/renderer/security-warnings.ts @electron/wg-security
|
||||
|
||||
# Infra WG
|
||||
/.claude/ @electron/wg-infra
|
||||
/.github/actions/ @electron/wg-infra
|
||||
/.github/workflows/*-publish.yml @electron/wg-infra
|
||||
/.github/workflows/build.yml @electron/wg-infra
|
||||
|
||||
52
.github/actions/build-electron/action.yml
vendored
52
.github/actions/build-electron/action.yml
vendored
@@ -47,6 +47,20 @@ runs:
|
||||
- name: Add Clang problem matcher
|
||||
shell: bash
|
||||
run: echo "::add-matcher::src/electron/.github/problem-matchers/clang.json"
|
||||
- name: Download previous object checksums
|
||||
uses: dawidd6/action-download-artifact@09b07ec687d10771279a426c79925ee415c12906 # v17
|
||||
if: ${{ (github.event_name == 'push' || github.event_name == 'pull_request') && inputs.is-asan != 'true' }}
|
||||
with:
|
||||
name: object_checksums_${{ inputs.artifact-platform }}_${{ inputs.target-arch }}
|
||||
commit: ${{ case(github.event_name == 'push', github.event.push.before, github.event.pull_request.base.sha) }}
|
||||
path: src
|
||||
if_no_artifact_found: ignore
|
||||
- name: Move previous object checksums
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -f src/object-checksums_${{ inputs.artifact-platform }}_${{ inputs.target-arch }}.json ]; then
|
||||
mv src/object-checksums_${{ inputs.artifact-platform }}_${{ inputs.target-arch }}.json src/previous-object-checksums.json
|
||||
fi
|
||||
- name: Build Electron ${{ inputs.step-suffix }}
|
||||
if: ${{ inputs.target-platform != 'win' }}
|
||||
shell: bash
|
||||
@@ -72,12 +86,17 @@ runs:
|
||||
cp out/Default/.ninja_log out/electron_ninja_log
|
||||
node electron/script/check-symlinks.js
|
||||
|
||||
# Upload build stats to Datadog
|
||||
if ! [ -z $DD_API_KEY ]; then
|
||||
npx node electron/script/build-stats.mjs out/Default/siso.INFO --upload-stats || true
|
||||
# Build stats and object checksums
|
||||
BUILD_STATS_ARGS="out/Default/siso.INFO --out-dir out/Default --output-object-checksums object-checksums.${{ inputs.artifact-platform }}_${{ inputs.target-arch }}.json"
|
||||
if [ -f previous-object-checksums.json ]; then
|
||||
BUILD_STATS_ARGS="$BUILD_STATS_ARGS --input-object-checksums previous-object-checksums.json"
|
||||
fi
|
||||
if ! [ -z "$DD_API_KEY" ]; then
|
||||
BUILD_STATS_ARGS="$BUILD_STATS_ARGS --upload-stats"
|
||||
else
|
||||
echo "Skipping build-stats.mjs upload because DD_API_KEY is not set"
|
||||
fi
|
||||
node electron/script/build-stats.mjs $BUILD_STATS_ARGS || true
|
||||
- name: Build Electron (Windows) ${{ inputs.step-suffix }}
|
||||
if: ${{ inputs.target-platform == 'win' }}
|
||||
shell: powershell
|
||||
@@ -95,16 +114,21 @@ runs:
|
||||
Copy-Item out\Default\.ninja_log out\electron_ninja_log
|
||||
node electron\script\check-symlinks.js
|
||||
|
||||
# Upload build stats to Datadog
|
||||
# Build stats and object checksums
|
||||
$statsArgs = @("out\Default\siso.exe.INFO", "--out-dir", "out\Default", "--output-object-checksums", "object-checksums.${{ inputs.artifact-platform }}_${{ inputs.target-arch }}.json")
|
||||
if (Test-Path previous-object-checksums.json) {
|
||||
$statsArgs += @("--input-object-checksums", "previous-object-checksums.json")
|
||||
}
|
||||
if ($env:DD_API_KEY) {
|
||||
try {
|
||||
npx node electron\script\build-stats.mjs out\Default\siso.exe.INFO --upload-stats ; $LASTEXITCODE = 0
|
||||
} catch {
|
||||
Write-Host "Build stats upload failed, continuing..."
|
||||
}
|
||||
$statsArgs += "--upload-stats"
|
||||
} else {
|
||||
Write-Host "Skipping build-stats.mjs upload because DD_API_KEY is not set"
|
||||
}
|
||||
try {
|
||||
& node electron\script\build-stats.mjs @statsArgs ; $LASTEXITCODE = 0
|
||||
} catch {
|
||||
Write-Host "Build stats failed, continuing..."
|
||||
}
|
||||
- name: Verify dist.zip ${{ inputs.step-suffix }}
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -128,6 +152,9 @@ runs:
|
||||
fi
|
||||
sed $SEDOPTION '/.*builtins-pgo/d' out/Default/mksnapshot_args
|
||||
sed $SEDOPTION '/--turbo-profiling-input/d' out/Default/mksnapshot_args
|
||||
sed $SEDOPTION '/--reorder-builtins/d' out/Default/mksnapshot_args
|
||||
sed $SEDOPTION '/--warn-about-builtin-profile-data/d' out/Default/mksnapshot_args
|
||||
sed $SEDOPTION '/--abort-on-bad-builtin-profile-data/d' out/Default/mksnapshot_args
|
||||
|
||||
if [ "${{ inputs.target-platform }}" = "win" ]; then
|
||||
cd out/Default
|
||||
@@ -289,3 +316,10 @@ runs:
|
||||
with:
|
||||
name: out_gen_artifacts_${{ env.ARTIFACT_KEY }}
|
||||
path: ./src/out/Default/gen
|
||||
- name: Upload Object Checksums ${{ inputs.step-suffix }}
|
||||
if: ${{ always() && !cancelled() && inputs.is-asan != 'true' }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: object_checksums_${{ inputs.artifact-platform }}_${{ inputs.target-arch }}
|
||||
path: ./src/object-checksums.${{ inputs.artifact-platform }}_${{ inputs.target-arch }}.json
|
||||
archive: false
|
||||
|
||||
6
.github/workflows/branch-created.yml
vendored
6
.github/workflows/branch-created.yml
vendored
@@ -157,7 +157,7 @@ jobs:
|
||||
}))
|
||||
- name: Create Release Project Board
|
||||
if: ${{ steps.check-major-version.outputs.MAJOR }}
|
||||
uses: dsanders11/project-actions/copy-project@2134fe7cc71c58b7ae259c82a8e63c6058255678 # v1.7.0
|
||||
uses: dsanders11/project-actions/copy-project@5767984408ccc6742f83acc8b8d8ea5e09f329af # v2.0.0
|
||||
id: create-release-board
|
||||
with:
|
||||
drafts: true
|
||||
@@ -177,7 +177,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||
- name: Find Previous Release Project Board
|
||||
if: ${{ steps.check-major-version.outputs.MAJOR }}
|
||||
uses: dsanders11/project-actions/find-project@2134fe7cc71c58b7ae259c82a8e63c6058255678 # v1.7.0
|
||||
uses: dsanders11/project-actions/find-project@5767984408ccc6742f83acc8b8d8ea5e09f329af # v2.0.0
|
||||
id: find-prev-release-board
|
||||
with:
|
||||
fail-if-project-not-found: false
|
||||
@@ -185,7 +185,7 @@ jobs:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
- name: Close Previous Release Project Board
|
||||
if: ${{ steps.find-prev-release-board.outputs.number }}
|
||||
uses: dsanders11/project-actions/close-project@2134fe7cc71c58b7ae259c82a8e63c6058255678 # v1.7.0
|
||||
uses: dsanders11/project-actions/close-project@5767984408ccc6742f83acc8b8d8ea5e09f329af # v2.0.0
|
||||
with:
|
||||
project-number: ${{ steps.find-prev-release-board.outputs.number }}
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
27
.github/workflows/build.yml
vendored
27
.github/workflows/build.yml
vendored
@@ -446,3 +446,30 @@ jobs:
|
||||
- name: GitHub Actions Jobs Done
|
||||
run: |
|
||||
echo "All GitHub Actions Jobs are done"
|
||||
|
||||
check-signed-commits:
|
||||
name: Check signed commits in green PR
|
||||
needs: gha-done
|
||||
if: ${{ contains(github.event.pull_request.labels.*.name, 'needs-signed-commits')}}
|
||||
runs-on: ubuntu-slim
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check signed commits in PR
|
||||
uses: 1Password/check-signed-commits-action@ed2885f3ed2577a4f5d3c3fe895432a557d23d52 # v1
|
||||
with:
|
||||
comment: |
|
||||
⚠️ This PR contains unsigned commits. This repository enforces [commit signatures](https://docs.github.com/en/authentication/managing-commit-signature-verification)
|
||||
for all incoming PRs. To get your PR merged, please sign those commits
|
||||
(`git rebase --exec 'git commit -S --amend --no-edit -n' @{upstream}`) and force push them to this branch
|
||||
(`git push --force-with-lease`)
|
||||
|
||||
For more information on signing commits, see GitHub's documentation on [Telling Git about your signing key](https://docs.github.com/en/authentication/managing-commit-signature-verification/telling-git-about-your-signing-key).
|
||||
|
||||
- name: Remove needs-signed-commits label
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
run: |
|
||||
gh pr edit $PR_URL --remove-label needs-signed-commits
|
||||
|
||||
24
.github/workflows/issue-commented.yml
vendored
24
.github/workflows/issue-commented.yml
vendored
@@ -34,30 +34,6 @@ jobs:
|
||||
run: |
|
||||
gh issue edit $ISSUE_URL --remove-label 'blocked/need-repro','blocked/need-info ❌'
|
||||
|
||||
pr-needs-signed-commits-commented:
|
||||
name: Remove needs-signed-commits on comment
|
||||
if: ${{ github.event.issue.pull_request && (contains(github.event.issue.labels.*.name, 'needs-signed-commits')) && (github.event.comment.user.login == github.event.issue.user.login) }}
|
||||
runs-on: ubuntu-slim
|
||||
steps:
|
||||
- name: Get author association
|
||||
id: get-author-association
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: *get-author-association
|
||||
- name: Generate GitHub App token
|
||||
uses: electron/github-app-auth-action@e14e47722ed120360649d0789e25b9baece12725 # v2.0.0
|
||||
if: ${{ !contains(fromJSON('["MEMBER", "OWNER", "COLLABORATOR"]'), steps.get-author-association.outputs.author_association) }}
|
||||
id: generate-token
|
||||
with:
|
||||
creds: ${{ secrets.ISSUE_TRIAGE_GH_APP_CREDS }}
|
||||
- name: Remove label
|
||||
if: ${{ !contains(fromJSON('["MEMBER", "OWNER", "COLLABORATOR"]'), steps.get-author-association.outputs.author_association) }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||
ISSUE_URL: ${{ github.event.issue.html_url }}
|
||||
run: |
|
||||
gh issue edit $ISSUE_URL --remove-label 'needs-signed-commits'
|
||||
|
||||
pr-reviewer-requested:
|
||||
name: Maintainer requested reviewer on PR
|
||||
if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/request-review') && github.event.comment.user.type != 'Bot' }}
|
||||
|
||||
6
.github/workflows/issue-labeled.yml
vendored
6
.github/workflows/issue-labeled.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
creds: ${{ secrets.ISSUE_TRIAGE_GH_APP_CREDS }}
|
||||
org: electron
|
||||
- name: Set status
|
||||
uses: dsanders11/project-actions/edit-item@2134fe7cc71c58b7ae259c82a8e63c6058255678 # v1.7.0
|
||||
uses: dsanders11/project-actions/edit-item@5767984408ccc6742f83acc8b8d8ea5e09f329af # v2.0.0
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
project-number: 90
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
creds: ${{ secrets.ISSUE_TRIAGE_GH_APP_CREDS }}
|
||||
org: electron
|
||||
- name: Set status
|
||||
uses: dsanders11/project-actions/edit-item@2134fe7cc71c58b7ae259c82a8e63c6058255678 # v1.7.0
|
||||
uses: dsanders11/project-actions/edit-item@5767984408ccc6742f83acc8b8d8ea5e09f329af # v2.0.0
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
project-number: 90
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
creds: ${{ secrets.ISSUE_TRIAGE_GH_APP_CREDS }}
|
||||
- name: Create comment
|
||||
if: ${{ steps.check-for-comment.outputs.SHOULD_COMMENT }}
|
||||
uses: actions-cool/issues-helper@71b62d7da76e59ff7b193904feb6e77d4dbb2777 # v3.7.6
|
||||
uses: actions-cool/issues-helper@200c78641dbf33838311e5a1e0c31bbdb92d7cf0 # v3.8.0
|
||||
with:
|
||||
actions: 'create-comment'
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
4
.github/workflows/issue-opened.yml
vendored
4
.github/workflows/issue-opened.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
creds: ${{ secrets.ISSUE_TRIAGE_GH_APP_CREDS }}
|
||||
org: electron
|
||||
- name: Add to Issue Triage
|
||||
uses: dsanders11/project-actions/add-item@2134fe7cc71c58b7ae259c82a8e63c6058255678 # v1.7.0
|
||||
uses: dsanders11/project-actions/add-item@5767984408ccc6742f83acc8b8d8ea5e09f329af # v2.0.0
|
||||
with:
|
||||
field: Reporter
|
||||
field-value: ${{ github.event.issue.user.login }}
|
||||
@@ -146,7 +146,7 @@ jobs:
|
||||
}
|
||||
- name: Create unsupported major comment
|
||||
if: ${{ steps.add-labels.outputs.unsupportedMajor }}
|
||||
uses: actions-cool/issues-helper@71b62d7da76e59ff7b193904feb6e77d4dbb2777 # v3.7.6
|
||||
uses: actions-cool/issues-helper@200c78641dbf33838311e5a1e0c31bbdb92d7cf0 # v3.8.0
|
||||
with:
|
||||
actions: 'create-comment'
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
2
.github/workflows/issue-transferred.yml
vendored
2
.github/workflows/issue-transferred.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
creds: ${{ secrets.ISSUE_TRIAGE_GH_APP_CREDS }}
|
||||
org: electron
|
||||
- name: Remove from issue triage
|
||||
uses: dsanders11/project-actions/delete-item@2134fe7cc71c58b7ae259c82a8e63c6058255678 # v1.7.0
|
||||
uses: dsanders11/project-actions/delete-item@5767984408ccc6742f83acc8b8d8ea5e09f329af # v2.0.0
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
project-number: 90
|
||||
|
||||
2
.github/workflows/issue-unlabeled.yml
vendored
2
.github/workflows/issue-unlabeled.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
org: electron
|
||||
- name: Set status
|
||||
if: ${{ steps.check-for-blocked-labels.outputs.NOT_BLOCKED }}
|
||||
uses: dsanders11/project-actions/edit-item@2134fe7cc71c58b7ae259c82a8e63c6058255678 # v1.7.0
|
||||
uses: dsanders11/project-actions/edit-item@5767984408ccc6742f83acc8b8d8ea5e09f329af # v2.0.0
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
project-number: 90
|
||||
|
||||
2
.github/workflows/pr-template-check.yml
vendored
2
.github/workflows/pr-template-check.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
sparse-checkout: .github/PULL_REQUEST_TEMPLATE.md
|
||||
sparse-checkout-cone-mode: false
|
||||
- name: Check for required sections
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
|
||||
2
.github/workflows/pr-triage-automation.yml
vendored
2
.github/workflows/pr-triage-automation.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
creds: ${{ secrets.ISSUE_TRIAGE_GH_APP_CREDS }}
|
||||
org: electron
|
||||
- name: Set status to Needs Review
|
||||
uses: dsanders11/project-actions/edit-item@2134fe7cc71c58b7ae259c82a8e63c6058255678 # v1.7.0
|
||||
uses: dsanders11/project-actions/edit-item@5767984408ccc6742f83acc8b8d8ea5e09f329af # v2.0.0
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
project-number: 118
|
||||
|
||||
4
.github/workflows/pull-request-labeled.yml
vendored
4
.github/workflows/pull-request-labeled.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
creds: ${{ secrets.RELEASE_BOARD_GH_APP_CREDS }}
|
||||
org: electron
|
||||
- name: Set status
|
||||
uses: dsanders11/project-actions/edit-item@2134fe7cc71c58b7ae259c82a8e63c6058255678 # v1.7.0
|
||||
uses: dsanders11/project-actions/edit-item@5767984408ccc6742f83acc8b8d8ea5e09f329af # v2.0.0
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
project-number: 94
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
with:
|
||||
creds: ${{ secrets.ISSUE_TRIAGE_GH_APP_CREDS }}
|
||||
- name: Create comment
|
||||
uses: actions-cool/issues-helper@71b62d7da76e59ff7b193904feb6e77d4dbb2777 # v3.7.6
|
||||
uses: actions-cool/issues-helper@200c78641dbf33838311e5a1e0c31bbdb92d7cf0 # v3.8.0
|
||||
with:
|
||||
actions: 'create-comment'
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
35
.github/workflows/pull-request-opened-synchronized.yml
vendored
Normal file
35
.github/workflows/pull-request-opened-synchronized.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: Pull Request Opened/Synchronized
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
check-signed-commits:
|
||||
name: Check signed commits in PR
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'needs-signed-commits')}}
|
||||
runs-on: ubuntu-slim
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check signed commits in PR
|
||||
uses: 1Password/check-signed-commits-action@ed2885f3ed2577a4f5d3c3fe895432a557d23d52 # v1
|
||||
with:
|
||||
comment: |
|
||||
⚠️ This PR contains unsigned commits. This repository enforces [commit signatures](https://docs.github.com/en/authentication/managing-commit-signature-verification)
|
||||
for all incoming PRs. To get your PR merged, please sign those commits
|
||||
(`git rebase --exec 'git commit -S --amend --no-edit -n' @{upstream}`) and force push them to this branch
|
||||
(`git push --force-with-lease`)
|
||||
|
||||
For more information on signing commits, see GitHub's documentation on [Telling Git about your signing key](https://docs.github.com/en/authentication/managing-commit-signature-verification/telling-git-about-your-signing-key).
|
||||
|
||||
- name: Add needs-signed-commits label
|
||||
if: ${{ failure() }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
run: |
|
||||
gh pr edit $PR_URL --add-label needs-signed-commits
|
||||
2
.github/workflows/scorecards.yml
vendored
2
.github/workflows/scorecards.yml
vendored
@@ -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@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v3.29.5
|
||||
uses: github/codeql-action/upload-sarif@38697555549f1db7851b81482ff19f1fa5c4fedc # v3.29.5
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
2
.github/workflows/stable-prep-items.yml
vendored
2
.github/workflows/stable-prep-items.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
PROJECT_NUMBER=$(gh project list --owner electron --format json | jq -r '.projects | map(select(.title | test("^[0-9]+-x-y$"))) | max_by(.number) | .number')
|
||||
echo "PROJECT_NUMBER=$PROJECT_NUMBER" >> "$GITHUB_OUTPUT"
|
||||
- name: Update Completed Stable Prep Items
|
||||
uses: dsanders11/project-actions/completed-by@2134fe7cc71c58b7ae259c82a8e63c6058255678 # v1.7.0
|
||||
uses: dsanders11/project-actions/completed-by@5767984408ccc6742f83acc8b8d8ea5e09f329af # v2.0.0
|
||||
with:
|
||||
field: Prep Status
|
||||
field-value: ✅ Complete
|
||||
|
||||
@@ -51,9 +51,6 @@ is_cfi = false
|
||||
use_qt5 = false
|
||||
use_qt6 = false
|
||||
|
||||
# Disables the builtins PGO for V8
|
||||
v8_builtins_profiling_log_file = ""
|
||||
|
||||
# https://chromium.googlesource.com/chromium/src/+/main/docs/dangling_ptr.md
|
||||
# TODO(vertedinde): hunt down dangling pointers on Linux
|
||||
enable_dangling_raw_ptr_checks = false
|
||||
|
||||
@@ -37,10 +37,11 @@ an issue:
|
||||
* [Represented File for macOS BrowserWindows](tutorial/represented-file.md)
|
||||
* [Native File Drag & Drop](tutorial/native-file-drag-drop.md)
|
||||
* [Navigation History](tutorial/navigation-history.md)
|
||||
* [Window State Persistence](tutorial/window-state-persistence.md)
|
||||
* [Offscreen Rendering](tutorial/offscreen-rendering.md)
|
||||
* [Dark Mode](tutorial/dark-mode.md)
|
||||
* [Web embeds in Electron](tutorial/web-embeds.md)
|
||||
* [Boilerplates and CLIs](tutorial/boilerplates-and-clis.md)
|
||||
* [Boilerplates and CLIs](tutorial/boilerplates-and-clis.md)
|
||||
* [Boilerplate vs CLI](tutorial/boilerplates-and-clis.md#boilerplate-vs-cli)
|
||||
* [Electron Forge](tutorial/boilerplates-and-clis.md#electron-forge)
|
||||
* [electron-builder](tutorial/boilerplates-and-clis.md#electron-builder)
|
||||
|
||||
@@ -373,6 +373,15 @@ Calling `event.preventDefault()` will prevent the menu from being displayed.
|
||||
|
||||
To convert `point` to DIP, use [`screen.screenToDipPoint(point)`](./screen.md#screenscreentodippointpoint-windows-linux).
|
||||
|
||||
#### Event: 'restored-persisted-state'
|
||||
|
||||
Emitted after the persisted window state has been restored.
|
||||
|
||||
Window state includes the window bounds (x, y, height, width) and display mode (maximized, fullscreen, kiosk).
|
||||
|
||||
> [!NOTE]
|
||||
> This event is only emitted when [windowStatePersistence](structures/window-state-persistence.md) is enabled in [BaseWindowConstructorOptions](structures/base-window-options.md) or in [BrowserWindowConstructorOptions](structures/browser-window-options.md).
|
||||
|
||||
### Static Methods
|
||||
|
||||
The `BaseWindow` class has the following static methods:
|
||||
@@ -391,6 +400,14 @@ Returns `BaseWindow | null` - The window that is focused in this application, ot
|
||||
|
||||
Returns `BaseWindow | null` - The window with the given `id`.
|
||||
|
||||
#### `BaseWindow.clearPersistedState(name)`
|
||||
|
||||
* `name` string - The window `name` to clear state for (see [BaseWindowConstructorOptions](structures/base-window-options.md)).
|
||||
|
||||
Clears the saved state for a window with the given name. This removes all persisted window bounds, display mode, and work area information that was previously saved when `windowStatePersistence` was enabled.
|
||||
|
||||
If the window `name` is empty or the window state doesn't exist, the method will log a warning.
|
||||
|
||||
### Instance Properties
|
||||
|
||||
Objects created with `new BaseWindow` have the following properties:
|
||||
|
||||
@@ -59,7 +59,12 @@ On Windows, returns true once the app has emitted the `ready` event.
|
||||
|
||||
### `safeStorage.isAsyncEncryptionAvailable()`
|
||||
|
||||
Returns `Promise<Boolean>` - Whether encryption is available for asynchronous safeStorage operations.
|
||||
Returns `Promise<boolean>` - Resolves with whether encryption is available for
|
||||
asynchronous safeStorage operations.
|
||||
|
||||
The asynchronous encryptor is initialized lazily the first time this method,
|
||||
`encryptStringAsync`, or `decryptStringAsync` is called after the app is ready.
|
||||
The returned promise resolves once initialization completes.
|
||||
|
||||
### `safeStorage.encryptString(plainText)`
|
||||
|
||||
|
||||
@@ -42,6 +42,8 @@
|
||||
Default is `false`.
|
||||
* `hiddenInMissionControl` boolean (optional) _macOS_ - Whether window should be hidden when the user toggles into mission control.
|
||||
* `kiosk` boolean (optional) - Whether the window is in kiosk mode. Default is `false`.
|
||||
* `name` string (optional) - A unique identifier for the window, used internally by Electron to enable features such as state persistence. Each window must have a distinct name. It can only be reused after the corresponding window has been destroyed. An error is thrown if the name is already in use. This is not the visible title shown to users on the title bar.
|
||||
* `windowStatePersistence` ([WindowStatePersistence](window-state-persistence.md) | boolean) (optional) - Configures or enables the persistence of window state (position, size, maximized state, etc.) across application restarts. Has no effect if window `name` is not provided. Automatically disabled when there is no available display. _Experimental_
|
||||
* `title` string (optional) - Default window title. Default is `"Electron"`. If the HTML tag `<title>` is defined in the HTML file loaded by `loadURL()`, this property will be ignored.
|
||||
* `icon` ([NativeImage](../native-image.md) | string) (optional) - The window icon. On Windows it is
|
||||
recommended to use `ICO` icons to get best visual effects, you can also
|
||||
@@ -94,7 +96,7 @@
|
||||
title bar and a full size content window, the traffic light buttons will
|
||||
display when being hovered over in the top left of the window.
|
||||
**Note:** This option is currently experimental.
|
||||
* `titleBarOverlay` Object | Boolean (optional) - When using a frameless window in conjunction with `win.setWindowButtonVisibility(true)` on macOS or using a `titleBarStyle` so that the standard window controls ("traffic lights" on macOS) are visible, this property enables the Window Controls Overlay [JavaScript APIs][overlay-javascript-apis] and [CSS Environment Variables][overlay-css-env-vars]. Specifying `true` will result in an overlay with default system colors. Default is `false`.
|
||||
* `titleBarOverlay` Object | boolean (optional) - When using a frameless window in conjunction with `win.setWindowButtonVisibility(true)` on macOS or using a `titleBarStyle` so that the standard window controls ("traffic lights" on macOS) are visible, this property enables the Window Controls Overlay [JavaScript APIs][overlay-javascript-apis] and [CSS Environment Variables][overlay-css-env-vars]. Specifying `true` will result in an overlay with default system colors. Default is `false`.
|
||||
* `color` String (optional) _Windows_ _Linux_ - The CSS color of the Window Controls Overlay when enabled. Default is the system color.
|
||||
* `symbolColor` String (optional) _Windows_ _Linux_ - The CSS color of the symbols on the Window Controls Overlay when enabled. Default is the system color.
|
||||
* `height` Integer (optional) - The height of the title bar and Window Controls Overlay in pixels. Default is system height.
|
||||
|
||||
4
docs/api/structures/window-state-persistence.md
Normal file
4
docs/api/structures/window-state-persistence.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# WindowStatePersistence Object
|
||||
|
||||
* `bounds` boolean (optional) - Whether to persist window position and size across application restarts. Defaults to `true` if not specified.
|
||||
* `displayMode` boolean (optional) - Whether to persist display modes (fullscreen, kiosk, maximized, etc.) across application restarts. Defaults to `true` if not specified.
|
||||
@@ -98,6 +98,9 @@ npm install electron --save-dev
|
||||
ELECTRON_INSTALL_PLATFORM=mas npx electron . --no
|
||||
```
|
||||
|
||||
This also means the `ELECTRON_SKIP_BINARY_DOWNLOAD` environment variable is no
|
||||
longer supported, as its primary purpose was to prevent the `postinstall` script from running.
|
||||
|
||||
### Removed: `quotas` object from `Session.clearStorageData(options)`
|
||||
|
||||
When calling `Session.clearStorageData(options)`, the `options.quotas` object is no longer supported because it has been
|
||||
|
||||
111
docs/development/multi-monitor-testing.md
Normal file
111
docs/development/multi-monitor-testing.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# Multi-Monitor Testing
|
||||
|
||||
The `virtualDisplay` addon leverages macOS CoreGraphics APIs to create virtual displays, allowing you to write and run multi-monitor tests without the need for physical monitors. Due to macOS CoreGraphics quirks, reading the entire guide once before writing tests is recommended.
|
||||
|
||||
## Methods
|
||||
|
||||
#### `virtualDisplay.create([options])`
|
||||
|
||||
Creates a virtual display and returns a display ID.
|
||||
|
||||
```js @ts-nocheck
|
||||
const virtualDisplay = require('@electron-ci/virtual-display')
|
||||
// Default: 1920×1080 at origin (0, 0)
|
||||
const displayId = virtualDisplay.create()
|
||||
```
|
||||
|
||||
```js @ts-nocheck
|
||||
const virtualDisplay = require('@electron-ci/virtual-display')
|
||||
// Custom options (all parameters optional and have default values)
|
||||
const displayId = virtualDisplay.create({
|
||||
width: 2560, // Display width in pixels
|
||||
height: 1440, // Display height in pixels
|
||||
x: 1920, // X position (top-left corner)
|
||||
y: 0 // Y position (top-left corner)
|
||||
})
|
||||
```
|
||||
|
||||
**Returns:** `number` - Unique display ID used to identify the display. Returns `0` on failure to create display.
|
||||
|
||||
> [!NOTE]
|
||||
> It is recommended to call [`virtualDisplay.forceCleanup()`](#virtualdisplayforcecleanup) before every test to prevent display creation from failing in that test. macOS CoreGraphics maintains an internal display ID allocation pool that can become corrupted when virtual displays are created and destroyed rapidly during testing. Without proper cleanup, subsequent display creation may fail with inconsistent display IDs, resulting in test flakiness.
|
||||
|
||||
#### `virtualDisplay.forceCleanup()`
|
||||
|
||||
Performs a complete cleanup of all virtual displays and resets the macOS CoreGraphics display system.
|
||||
|
||||
```js @ts-nocheck
|
||||
beforeEach(() => {
|
||||
virtualDisplay.forceCleanup()
|
||||
})
|
||||
```
|
||||
|
||||
#### `virtualDisplay.destroy(displayId)`
|
||||
|
||||
Removes the virtual display.
|
||||
|
||||
```js @ts-nocheck
|
||||
virtualDisplay.destroy(displayId)
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Always destroy virtual displays after use to prevent corrupting the macOS CoreGraphics display pool and affecting subsequent tests.
|
||||
|
||||
## Recommended usage pattern
|
||||
|
||||
```js @ts-nocheck
|
||||
describe('multi-monitor tests', () => {
|
||||
const virtualDisplay = require('@electron-ci/virtual-display')
|
||||
beforeEach(() => {
|
||||
virtualDisplay.forceCleanup()
|
||||
})
|
||||
|
||||
it('should handle multiple displays', () => {
|
||||
const display1 = virtualDisplay.create({ width: 1920, height: 1080, x: 0, y: 0 })
|
||||
const display2 = virtualDisplay.create({ width: 2560, height: 1440, x: 1920, y: 0 })
|
||||
// Your test logic here
|
||||
virtualDisplay.destroy(display1)
|
||||
virtualDisplay.destroy(display2)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Display Constraints
|
||||
|
||||
### Size Limits
|
||||
|
||||
Virtual displays are constrained to 720×720 pixels minimum and 8192×8192 pixels maximum. Actual limits may vary depending on your Mac's graphics capabilities, so sizes outside this range (like 9000×6000) may fail on some systems.
|
||||
|
||||
```js @ts-nocheck
|
||||
// Safe sizes for testing
|
||||
virtualDisplay.create({ width: 1920, height: 1080 }) // Full HD
|
||||
virtualDisplay.create({ width: 3840, height: 2160 }) // 4K
|
||||
```
|
||||
|
||||
### Positioning Behavior
|
||||
|
||||
macOS maintains a contiguous desktop space by automatically adjusting display positions if there are any overlaps or gaps. In case of either, the placement of the new origin is as close as possible to the requested location, without overlapping or leaving a gap between displays.
|
||||
|
||||
**Overlap:**
|
||||
|
||||
```js @ts-nocheck
|
||||
// Requested positions
|
||||
const display1 = virtualDisplay.create({ x: 0, y: 0, width: 1920, height: 1080 })
|
||||
const display2 = virtualDisplay.create({ x: 500, y: 0, width: 1920, height: 1080 })
|
||||
|
||||
// macOS automatically repositions display2 to x: 1920 to prevent overlap
|
||||
const actualBounds = screen.getAllDisplays().map(d => d.bounds)
|
||||
// Result: [{ x: 0, y: 0, width: 1920, height: 1080 }, { x: 1920, y: 0, width: 1920, height: 1080 }]
|
||||
```
|
||||
|
||||
**Gap:**
|
||||
|
||||
```js @ts-nocheck
|
||||
// Requested: gap between displays
|
||||
const display1 = virtualDisplay.create({ width: 1920, height: 1080, x: 0, y: 0 })
|
||||
const display2 = virtualDisplay.create({ width: 1920, height: 1080, x: 2000, y: 0 })
|
||||
// macOS snaps display2 to x: 1920 (eliminates 80px gap)
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Always verify actual positions with `screen.getAllDisplays()` after creation, as macOS may adjust coordinates from the set values.
|
||||
@@ -95,3 +95,11 @@ To configure display scaling:
|
||||
|
||||
1. Push the Windows key and search for _Display settings_.
|
||||
2. Under _Scale and layout_, make sure that the device is set to 100%.
|
||||
|
||||
## Multi-Monitor Tests
|
||||
|
||||
Some Electron APIs require testing across multiple displays, such as screen detection, window positioning, and display-related events. For contributors working on these features, the `virtualDisplay` native addon enables you to create and position virtual displays programmatically, making it possible to test multi-monitor scenarios without any physical hardware.
|
||||
|
||||
For detailed information on using virtual displays in your tests, see [Multi-Monitor Testing](multi-monitor-testing.md).
|
||||
|
||||
**Platform support:** macOS only
|
||||
|
||||
@@ -25,16 +25,6 @@ included in the `electron` package:
|
||||
npx install-electron --no
|
||||
```
|
||||
|
||||
If you want to install your project's dependencies but don't need to use
|
||||
Electron functionality, you can set the `ELECTRON_SKIP_BINARY_DOWNLOAD` environment
|
||||
variable to prevent the binary from being downloaded. For instance, this feature can
|
||||
be useful in continuous integration environments when running unit tests that mock
|
||||
out the `electron` module.
|
||||
|
||||
```sh
|
||||
ELECTRON_SKIP_BINARY_DOWNLOAD=1 npm install
|
||||
```
|
||||
|
||||
## Running Electron ad-hoc
|
||||
|
||||
If you're in a pinch and would prefer to not use `npm install` in your local
|
||||
|
||||
@@ -87,6 +87,13 @@ if (!gotTheLock) {
|
||||
// Create mainWindow, load the rest of the app, etc...
|
||||
app.whenReady().then(() => {
|
||||
createWindow()
|
||||
// Check for deep link on cold start
|
||||
if (process.argv.length >= 2) {
|
||||
const lastArg = process.argv[process.argv.length - 1]
|
||||
if (lastArg.startsWith('electron-fiddle://')) {
|
||||
dialog.showErrorBox('Welcome Back', `You arrived from: ${lastArg}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
102
docs/tutorial/window-state-persistence.md
Normal file
102
docs/tutorial/window-state-persistence.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# Window State Persistence
|
||||
|
||||
## Overview
|
||||
|
||||
Window State Persistence allows your Electron application to automatically save and restore a window's position, size, and display modes (such as maximized or fullscreen states) across application restarts.
|
||||
|
||||
This feature is particularly useful for applications where users frequently resize, move, or maximize windows and expect them to remain in the same state when reopening the app.
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic usage
|
||||
|
||||
To enable Window State Persistence, simply set `windowStatePersistence: true` in your window constructor options and provide a unique `name` for the window.
|
||||
|
||||
```js
|
||||
const { app, BrowserWindow } = require('electron')
|
||||
|
||||
function createWindow () {
|
||||
const win = new BrowserWindow({
|
||||
name: 'main-window',
|
||||
width: 800,
|
||||
height: 600,
|
||||
windowStatePersistence: true
|
||||
})
|
||||
|
||||
win.loadFile('index.html')
|
||||
}
|
||||
|
||||
app.whenReady().then(createWindow)
|
||||
```
|
||||
|
||||
With this configuration, Electron will automatically:
|
||||
|
||||
1. Restore the window's position, size, and display mode when created (if a previous state exists)
|
||||
2. Save the window state whenever it changes (position, size, or display mode).
|
||||
3. Emit a `restored-persisted-state` event after successfully restoring state.
|
||||
4. Adapt restored window state to multi-monitor setups and display changes automatically.
|
||||
|
||||
> [!NOTE]
|
||||
> Window State Persistence requires that the window has a unique `name` property set in its constructor options. This name serves as the identifier for storing and retrieving the window's saved state.
|
||||
|
||||
### Selective persistence
|
||||
|
||||
You can control which aspects of the window state are persisted by passing an object with specific options:
|
||||
|
||||
```js
|
||||
const { app, BrowserWindow } = require('electron')
|
||||
|
||||
function createWindow () {
|
||||
const win = new BrowserWindow({
|
||||
name: 'main-window',
|
||||
width: 800,
|
||||
height: 600,
|
||||
windowStatePersistence: {
|
||||
bounds: true, // Save position and size (default: true)
|
||||
displayMode: false // Don't save maximized/fullscreen/kiosk state (default: true)
|
||||
}
|
||||
})
|
||||
|
||||
win.loadFile('index.html')
|
||||
}
|
||||
|
||||
app.whenReady().then(createWindow)
|
||||
```
|
||||
|
||||
In this example, the window will remember its position and size but will always start in normal mode, even if it was maximized or fullscreened when last closed.
|
||||
|
||||
### Clearing persisted state
|
||||
|
||||
You can programmatically clear the saved state for a specific window using the static `clearPersistedState` method:
|
||||
|
||||
```js
|
||||
const { BrowserWindow } = require('electron')
|
||||
|
||||
// Clear saved state for a specific window
|
||||
BrowserWindow.clearPersistedState('main-window')
|
||||
|
||||
// Now when you create a window with this name,
|
||||
// it will use the default constructor options
|
||||
const win = new BrowserWindow({
|
||||
name: 'main-window',
|
||||
width: 800,
|
||||
height: 600,
|
||||
windowStatePersistence: true
|
||||
})
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
The Window State Persistence APIs are available on both `BaseWindow` and `BrowserWindow` (since `BrowserWindow` extends `BaseWindow`) and work identically.
|
||||
|
||||
For complete API documentation, see:
|
||||
|
||||
- [`windowStatePersistence` in BaseWindowConstructorOptions][base-window-options]
|
||||
- [`WindowStatePersistence` object structure][window-state-persistence-structure]
|
||||
- [`BaseWindow.clearPersistedState()`][clear-persisted-state]
|
||||
- [`restored-persisted-state` event][restored-event]
|
||||
|
||||
[base-window-options]: ../api/structures/base-window-options.md
|
||||
[window-state-persistence-structure]: ../api/structures/window-state-persistence.md
|
||||
[clear-persisted-state]: ../api/base-window.md#basewindowclearpersistedstatename
|
||||
[restored-event]: ../api/base-window.md#event-restored-persisted-state
|
||||
@@ -172,6 +172,7 @@ auto_filenames = {
|
||||
"docs/api/structures/web-source.md",
|
||||
"docs/api/structures/window-open-handler-response.md",
|
||||
"docs/api/structures/window-session-end-event.md",
|
||||
"docs/api/structures/window-state-persistence.md",
|
||||
]
|
||||
|
||||
sandbox_bundle_deps = [
|
||||
|
||||
@@ -111,6 +111,8 @@ BrowserWindow.getAllWindows = () => {
|
||||
return BaseWindow.getAllWindows().filter(isBrowserWindow) as any[] as BWT[];
|
||||
};
|
||||
|
||||
BrowserWindow.clearPersistedState = BaseWindow.clearPersistedState;
|
||||
|
||||
BrowserWindow.getFocusedWindow = () => {
|
||||
for (const window of BrowserWindow.getAllWindows()) {
|
||||
if (!window.isDestroyed() && window.webContents && !window.webContents.isDestroyed()) {
|
||||
|
||||
@@ -17,11 +17,6 @@ export type WindowOpenArgs = {
|
||||
features: string,
|
||||
}
|
||||
|
||||
const frameNamesToWindow = new Map<string, WebContents>();
|
||||
const registerFrameNameToGuestWindow = (name: string, webContents: WebContents) => frameNamesToWindow.set(name, webContents);
|
||||
const unregisterFrameName = (name: string) => frameNamesToWindow.delete(name);
|
||||
const getGuestWebContentsByFrameName = (name: string) => frameNamesToWindow.get(name);
|
||||
|
||||
/**
|
||||
* `openGuestWindow` is called to create and setup event handling for the new
|
||||
* window.
|
||||
@@ -47,20 +42,6 @@ export function openGuestWindow ({ embedder, guest, referrer, disposition, postD
|
||||
...overrideBrowserWindowOptions
|
||||
};
|
||||
|
||||
// To spec, subsequent window.open calls with the same frame name (`target` in
|
||||
// spec parlance) will reuse the previous window.
|
||||
// https://html.spec.whatwg.org/multipage/window-object.html#apis-for-creating-and-navigating-browsing-contexts-by-name
|
||||
const existingWebContents = getGuestWebContentsByFrameName(frameName);
|
||||
if (existingWebContents) {
|
||||
if (existingWebContents.isDestroyed()) {
|
||||
// FIXME(t57ser): The webContents is destroyed for some reason, unregister the frame name
|
||||
unregisterFrameName(frameName);
|
||||
} else {
|
||||
existingWebContents.loadURL(url);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (createWindow) {
|
||||
const webContents = createWindow({
|
||||
webContents: guest,
|
||||
@@ -72,7 +53,7 @@ export function openGuestWindow ({ embedder, guest, referrer, disposition, postD
|
||||
throw new Error('Invalid webContents. Created window should be connected to webContents passed with options object.');
|
||||
}
|
||||
|
||||
handleWindowLifecycleEvents({ embedder, frameName, guest, outlivesOpener });
|
||||
handleWindowLifecycleEvents({ embedder, guest, outlivesOpener });
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -96,7 +77,7 @@ export function openGuestWindow ({ embedder, guest, referrer, disposition, postD
|
||||
});
|
||||
}
|
||||
|
||||
handleWindowLifecycleEvents({ embedder, frameName, guest: window.webContents, outlivesOpener });
|
||||
handleWindowLifecycleEvents({ embedder, guest: window.webContents, outlivesOpener });
|
||||
|
||||
embedder.emit('did-create-window', window, { url, frameName, options: browserWindowOptions, disposition, referrer, postData });
|
||||
}
|
||||
@@ -107,10 +88,9 @@ export function openGuestWindow ({ embedder, guest, referrer, disposition, postD
|
||||
* too is the guest destroyed; this is Electron convention and isn't based in
|
||||
* browser behavior.
|
||||
*/
|
||||
const handleWindowLifecycleEvents = function ({ embedder, guest, frameName, outlivesOpener }: {
|
||||
const handleWindowLifecycleEvents = function ({ embedder, guest, outlivesOpener }: {
|
||||
embedder: WebContents,
|
||||
guest: WebContents,
|
||||
frameName: string,
|
||||
outlivesOpener: boolean
|
||||
}) {
|
||||
const closedByEmbedder = function () {
|
||||
@@ -128,13 +108,6 @@ const handleWindowLifecycleEvents = function ({ embedder, guest, frameName, outl
|
||||
embedder.once('current-render-view-deleted' as any, closedByEmbedder);
|
||||
}
|
||||
guest.once('destroyed', closedByUser);
|
||||
|
||||
if (frameName) {
|
||||
registerFrameNameToGuestWindow(frameName, guest);
|
||||
guest.once('destroyed', function () {
|
||||
unregisterFrameName(frameName);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Security options that child windows will always inherit from parent windows
|
||||
|
||||
@@ -11,10 +11,6 @@ const path = require('path');
|
||||
|
||||
const { version } = require('./package');
|
||||
|
||||
if (process.env.ELECTRON_SKIP_BINARY_DOWNLOAD) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const platformPath = getPlatformPath();
|
||||
|
||||
if (isInstalled()) {
|
||||
|
||||
@@ -52,7 +52,6 @@ adjust_accessibility_ui_for_electron.patch
|
||||
worker_feat_add_hook_to_notify_script_ready.patch
|
||||
chore_provide_iswebcontentscreationoverridden_with_full_params.patch
|
||||
fix_properly_honor_printing_page_ranges.patch
|
||||
export_gin_v8platform_pageallocator_for_usage_outside_of_the_gin.patch
|
||||
fix_export_zlib_symbols.patch
|
||||
web_contents.patch
|
||||
webview_fullscreen.patch
|
||||
@@ -104,7 +103,6 @@ chore_remove_check_is_test_on_script_injection_tracker.patch
|
||||
fix_restore_original_resize_performance_on_macos.patch
|
||||
feat_allow_code_cache_in_custom_schemes.patch
|
||||
build_run_reclient_cfg_generator_after_chrome.patch
|
||||
fix_getcursorscreenpoint_wrongly_returns_0_0.patch
|
||||
fix_add_support_for_skipping_first_2_no-op_refreshes_in_thumb_cap.patch
|
||||
refactor_expose_file_system_access_blocklist.patch
|
||||
feat_add_support_for_missing_dialog_features_to_shell_dialogs.patch
|
||||
@@ -149,3 +147,5 @@ fix_pass_trigger_for_global_shortcuts_on_wayland.patch
|
||||
feat_plumb_node_integration_in_worker_through_workersettings.patch
|
||||
fix_restore_sdk_inputs_cross-toolchain_deps_for_macos.patch
|
||||
fix_use_fresh_lazynow_for_onendworkitemimpl_after_didruntask.patch
|
||||
fix_pulseaudio_stream_and_icon_names.patch
|
||||
fix_fire_menu_popup_start_for_dynamically_created_aria_menus.patch
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Samuel Attard <samuel.r.attard@gmail.com>
|
||||
Date: Tue, 3 Nov 2020 16:49:32 -0800
|
||||
Subject: export gin::V8Platform::PageAllocator for usage outside of the gin
|
||||
platform
|
||||
|
||||
In order for memory allocation in the main process node environment to be
|
||||
correctly tagged with MAP_JIT we need to use gins page allocator instead
|
||||
of the default V8 allocator. This probably can't be usptreamed.
|
||||
|
||||
diff --git a/gin/public/v8_platform.h b/gin/public/v8_platform.h
|
||||
index 8c32005730153251e93516340e4baa500d777178..ff444dc689542a909ec5aada39816931b3320921 100644
|
||||
--- a/gin/public/v8_platform.h
|
||||
+++ b/gin/public/v8_platform.h
|
||||
@@ -32,6 +32,7 @@ class GIN_EXPORT V8Platform : public v8::Platform {
|
||||
// enabling Arm's Branch Target Instructions for executable pages. This is
|
||||
// verified in the tests for gin::PageAllocator.
|
||||
PageAllocator* GetPageAllocator() override;
|
||||
+ static PageAllocator* GetCurrentPageAllocator();
|
||||
#if PA_BUILDFLAG(ENABLE_THREAD_ISOLATION)
|
||||
ThreadIsolatedAllocator* GetThreadIsolatedAllocator() override;
|
||||
#endif
|
||||
diff --git a/gin/v8_platform.cc b/gin/v8_platform.cc
|
||||
index fe339f6a069064ec92bddd5df9df96f84d13bd9a..41bc93d602c6558620ec728ac8207dedbabdd407 100644
|
||||
--- a/gin/v8_platform.cc
|
||||
+++ b/gin/v8_platform.cc
|
||||
@@ -222,6 +222,10 @@ ThreadIsolatedAllocator* V8Platform::GetThreadIsolatedAllocator() {
|
||||
}
|
||||
#endif // PA_BUILDFLAG(ENABLE_THREAD_ISOLATION)
|
||||
|
||||
+PageAllocator* V8Platform::GetCurrentPageAllocator() {
|
||||
+ return g_page_allocator.Pointer();
|
||||
+}
|
||||
+
|
||||
void V8Platform::OnCriticalMemoryPressure() {
|
||||
// We only have a reservation on 32-bit Windows systems.
|
||||
// TODO(bbudge) Make the #if's in BlinkInitializer match.
|
||||
@@ -0,0 +1,95 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Keeley Hammond <khammond@slack-corp.com>
|
||||
Date: Thu, 19 Mar 2026 00:34:37 -0700
|
||||
Subject: fix: fire MENU_POPUP_START for dynamically created ARIA menus
|
||||
|
||||
When an ARIA menu element is dynamically created (e.g. via appendChild)
|
||||
rather than being shown by toggling visibility, the AXMenuOpened event
|
||||
was not fired. The OnIgnoredChanged path handles the visibility toggle
|
||||
case, but OnAtomicUpdateFinished did not fire MENU_POPUP_START for
|
||||
newly created menu nodes.
|
||||
|
||||
Previous attempts to fix this (crbug.com/1254875) were reverted because
|
||||
they fired the event too eagerly in OnNodeCreated (before the tree was
|
||||
fully formed) and without filtering, causing regressions with screen
|
||||
readers on pages that misused role="menu".
|
||||
|
||||
This fix addresses both issues:
|
||||
1. Fires MENU_POPUP_START in OnAtomicUpdateFinished (after the tree
|
||||
update is complete) rather than in OnNodeCreated.
|
||||
2. Only fires if the menu has at least one menuitem child, filtering
|
||||
out false positives from misused role="menu" elements.
|
||||
|
||||
MENU_POPUP_END for deleted menus is already handled by
|
||||
AXTreeManager::OnNodeWillBeDeleted, which fires the event directly
|
||||
on the menu node before destruction.
|
||||
|
||||
The change is behind the DynamicMenuPopupEvents feature flag, disabled
|
||||
by default, to allow stabilization before enabling by default. Enable
|
||||
with --enable-features=DynamicMenuPopupEvents.
|
||||
|
||||
This patch can be removed when a CL containing the fix is accepted
|
||||
into Chromium.
|
||||
|
||||
Bug: 40794596
|
||||
|
||||
diff --git a/ui/accessibility/ax_event_generator.cc b/ui/accessibility/ax_event_generator.cc
|
||||
index 8fe1cacc274c543e6a5f13bb9b3712639f8bbda5..c87c47f34a5f47e9cb7cec04d703335a57f250cd 100644
|
||||
--- a/ui/accessibility/ax_event_generator.cc
|
||||
+++ b/ui/accessibility/ax_event_generator.cc
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "ui/accessibility/ax_event_generator.h"
|
||||
|
||||
+#include "base/feature_list.h"
|
||||
#include "base/no_destructor.h"
|
||||
#include "ui/accessibility/ax_enums.mojom.h"
|
||||
#include "ui/accessibility/ax_event.h"
|
||||
@@ -12,6 +13,12 @@
|
||||
|
||||
namespace ui {
|
||||
|
||||
+// Feature flag for firing MENU_POPUP_START for dynamically created ARIA menus.
|
||||
+// Disabled by default to allow stabilization before enabling globally.
|
||||
+BASE_FEATURE(kDynamicMenuPopupEvents,
|
||||
+ "DynamicMenuPopupEvents",
|
||||
+ base::FEATURE_DISABLED_BY_DEFAULT);
|
||||
+
|
||||
namespace {
|
||||
|
||||
bool HasEvent(const std::set<AXEventGenerator::EventParams>& node_events,
|
||||
@@ -1011,12 +1018,31 @@ void AXEventGenerator::OnAtomicUpdateFinished(
|
||||
/*new_value*/ true);
|
||||
}
|
||||
|
||||
- if (IsAlert(change.node->GetRole()))
|
||||
+ if (IsAlert(change.node->GetRole())) {
|
||||
AddEvent(change.node, Event::ALERT);
|
||||
- else if (change.node->data().IsActiveLiveRegionRoot())
|
||||
+ } else if (change.node->data().IsActiveLiveRegionRoot()) {
|
||||
AddEvent(change.node, Event::LIVE_REGION_CREATED);
|
||||
- else if (change.node->data().IsContainedInActiveLiveRegion())
|
||||
+ } else if (change.node->data().IsContainedInActiveLiveRegion()) {
|
||||
FireLiveRegionEvents(change.node, /* is_removal */ false);
|
||||
+ }
|
||||
+
|
||||
+ // Fire MENU_POPUP_START when a menu is dynamically created (e.g. via
|
||||
+ // appendChild). The OnIgnoredChanged path handles menus that already exist
|
||||
+ // in the DOM and are shown/hidden. This handles the case where the menu
|
||||
+ // element itself is created on the fly.
|
||||
+ // Only fire if the menu has at least one menuitem child, to avoid false
|
||||
+ // positives from elements that misuse role="menu".
|
||||
+ if (base::FeatureList::IsEnabled(kDynamicMenuPopupEvents) &&
|
||||
+ change.node->GetRole() == ax::mojom::Role::kMenu &&
|
||||
+ !change.node->IsInvisibleOrIgnored()) {
|
||||
+ for (auto iter = change.node->UnignoredChildrenBegin();
|
||||
+ iter != change.node->UnignoredChildrenEnd(); ++iter) {
|
||||
+ if (IsMenuItem(iter->GetRole())) {
|
||||
+ AddEvent(change.node, Event::MENU_POPUP_START);
|
||||
+ break;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
}
|
||||
|
||||
FireActiveDescendantEvents();
|
||||
@@ -1,25 +0,0 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Charles Kerr <charles@charleskerr.com>
|
||||
Date: Thu, 8 Feb 2024 00:41:40 -0600
|
||||
Subject: fix: GetCursorScreenPoint() wrongly returns 0, 0
|
||||
|
||||
Fixes #41143. Discussion of the issue at
|
||||
https://github.com/electron/electron/issues/41143#issuecomment-1933443163
|
||||
|
||||
This patch should be backported to e29, upstreamed to Chromium, and then
|
||||
removed if it lands upstream.
|
||||
|
||||
diff --git a/ui/events/x/events_x_utils.cc b/ui/events/x/events_x_utils.cc
|
||||
index 185c9dbc22237d330b1c2020cae93ffcda5de6fa..0f6c98411feecda79e26b52e4d889d6e61b550ae 100644
|
||||
--- a/ui/events/x/events_x_utils.cc
|
||||
+++ b/ui/events/x/events_x_utils.cc
|
||||
@@ -608,6 +608,9 @@ gfx::Point EventLocationFromXEvent(const x11::Event& xev) {
|
||||
gfx::Point EventSystemLocationFromXEvent(const x11::Event& xev) {
|
||||
if (auto* crossing = xev.As<x11::CrossingEvent>())
|
||||
return gfx::Point(crossing->root_x, crossing->root_y);
|
||||
+ if (auto* crossing = xev.As<x11::Input::CrossingEvent>())
|
||||
+ return gfx::Point(Fp1616ToDouble(crossing->root_x),
|
||||
+ Fp1616ToDouble(crossing->root_y));
|
||||
if (auto* button = xev.As<x11::ButtonEvent>())
|
||||
return gfx::Point(button->root_x, button->root_y);
|
||||
if (auto* motion = xev.As<x11::MotionNotifyEvent>())
|
||||
120
patches/chromium/fix_pulseaudio_stream_and_icon_names.patch
Normal file
120
patches/chromium/fix_pulseaudio_stream_and_icon_names.patch
Normal file
@@ -0,0 +1,120 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Damglador <vse.stopchanskyi@gmail.com>
|
||||
Date: Fri, 26 Dec 2025 21:26:43 +0100
|
||||
Subject: fix: pulseaudio stream and icon names
|
||||
|
||||
Use platform_util::GetXdgAppId() with fallback to argv0 as PA_PROP_APPLICATION_ICON_NAME.
|
||||
Use electron::GetPossiblyOverriddenApplicationName()
|
||||
to set environment variable "ELECTRON_PA_APP_NAME" in audio_service.cc,
|
||||
to use it in pulse_util.cc for setting input/output pa_context name.
|
||||
|
||||
This replaces hard-codded kBrowserDisplayName that was used for PA_PROP_APPLICATION_ICON_NAME,
|
||||
and PRODUCT_STRING that was used for pa_context names.
|
||||
|
||||
This is done to make audio streams recognizable in tools like qpwgrapth and general audio managers,
|
||||
instead of having 20 "Chromium" outputs and "Chromium input" inputs, that are actually coming from
|
||||
completely different applications.
|
||||
|
||||
This patch can be removed when upstream starts using AudioManager::SetGlobalAppName()
|
||||
for all pa_context names (and when actually works with AudioServiceOutOfProcess).
|
||||
|
||||
diff --git a/content/browser/audio/audio_service.cc b/content/browser/audio/audio_service.cc
|
||||
index 70615782c50d18606c3baa42a223e54f8619bc07..fb67e69f9ff46b432236b46913a1b10dd8302887 100644
|
||||
--- a/content/browser/audio/audio_service.cc
|
||||
+++ b/content/browser/audio/audio_service.cc
|
||||
@@ -29,6 +29,9 @@
|
||||
#include "services/audio/public/mojom/audio_service.mojom.h"
|
||||
#include "services/audio/service.h"
|
||||
#include "services/audio/service_factory.h"
|
||||
+#if BUILDFLAG(IS_LINUX)
|
||||
+#include "electron/shell/common/application_info.h"
|
||||
+#endif
|
||||
|
||||
#if BUILDFLAG(ENABLE_PASSTHROUGH_AUDIO_CODECS) && BUILDFLAG(IS_WIN)
|
||||
#define PASS_EDID_ON_COMMAND_LINE 1
|
||||
@@ -109,6 +112,10 @@ void LaunchAudioServiceOutOfProcess(
|
||||
mojo::PendingReceiver<audio::mojom::AudioService> receiver,
|
||||
uint32_t codec_bitmask) {
|
||||
std::vector<std::string> switches;
|
||||
+#if BUILDFLAG(IS_LINUX)
|
||||
+ // Set ELECTRON_PA_APP_NAME variable for pulse_util to grab and set pa_context name
|
||||
+ setenv("ELECTRON_PA_APP_NAME", electron::GetPossiblyOverriddenApplicationName().c_str(), 1);
|
||||
+#endif
|
||||
#if BUILDFLAG(IS_MAC)
|
||||
// On Mac, the audio service requires a CFRunLoop provided by a
|
||||
// UI MessageLoop type, to run AVFoundation and CoreAudio code.
|
||||
diff --git a/media/audio/pulse/pulse_util.cc b/media/audio/pulse/pulse_util.cc
|
||||
index a08e42a464a3894cbf2b8e3cf8a320a33423b719..e5d69506e1585710a2540c91ca51cba7a4692575 100644
|
||||
--- a/media/audio/pulse/pulse_util.cc
|
||||
+++ b/media/audio/pulse/pulse_util.cc
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
|
||||
+#include "base/command_line.h"
|
||||
#include "base/files/file_path.h"
|
||||
#include "base/logging.h"
|
||||
#include "base/memory/ptr_util.h"
|
||||
@@ -20,6 +21,7 @@
|
||||
#include "build/branding_buildflags.h"
|
||||
#include "media/audio/audio_device_description.h"
|
||||
#include "media/base/audio_timestamp_helper.h"
|
||||
+#include "electron/shell/common/platform_util.h"
|
||||
|
||||
#if defined(DLOPEN_PULSEAUDIO)
|
||||
#include "media/audio/pulse/pulse_stubs.h"
|
||||
@@ -36,10 +38,8 @@ namespace pulse {
|
||||
namespace {
|
||||
|
||||
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
|
||||
-constexpr char kBrowserDisplayName[] = "google-chrome";
|
||||
#define PRODUCT_STRING "Google Chrome"
|
||||
#else
|
||||
-constexpr char kBrowserDisplayName[] = "chromium-browser";
|
||||
#define PRODUCT_STRING "Chromium"
|
||||
#endif
|
||||
|
||||
@@ -236,7 +236,7 @@ bool InitPulse(pa_threaded_mainloop** mainloop, pa_context** context) {
|
||||
|
||||
pa_mainloop_api* pa_mainloop_api = pa_threaded_mainloop_get_api(pa_mainloop);
|
||||
pa_context* pa_context =
|
||||
- pa_context_new(pa_mainloop_api, PRODUCT_STRING " input");
|
||||
+ pa_context_new(pa_mainloop_api, getenv("ELECTRON_PA_APP_NAME"));
|
||||
if (!pa_context) {
|
||||
pa_threaded_mainloop_free(pa_mainloop);
|
||||
return false;
|
||||
@@ -464,8 +464,11 @@ bool CreateInputStream(pa_threaded_mainloop* mainloop,
|
||||
// Create a new recording stream and
|
||||
// tells PulseAudio what the stream icon should be.
|
||||
ScopedPropertyList property_list;
|
||||
+ const std::string cmd_name =
|
||||
+ base::CommandLine::ForCurrentProcess()->GetProgram().BaseName().value();
|
||||
+ const std::string app_id = platform_util::GetXdgAppId().value_or(cmd_name);
|
||||
pa_proplist_sets(property_list.get(), PA_PROP_APPLICATION_ICON_NAME,
|
||||
- kBrowserDisplayName);
|
||||
+ app_id.c_str());
|
||||
*stream = pa_stream_new_with_proplist(context, "RecordStream",
|
||||
&sample_specifications, map,
|
||||
property_list.get());
|
||||
@@ -526,7 +529,7 @@ bool CreateOutputStream(raw_ptr<pa_threaded_mainloop>* mainloop,
|
||||
|
||||
pa_mainloop_api* pa_mainloop_api = pa_threaded_mainloop_get_api(*mainloop);
|
||||
*context = pa_context_new(
|
||||
- pa_mainloop_api, app_name.empty() ? PRODUCT_STRING : app_name.c_str());
|
||||
+ pa_mainloop_api, getenv("ELECTRON_PA_APP_NAME"));
|
||||
RETURN_ON_FAILURE(*context, "Failed to create PulseAudio context.");
|
||||
|
||||
// A state callback must be set before calling pa_threaded_mainloop_lock() or
|
||||
@@ -574,8 +577,11 @@ bool CreateOutputStream(raw_ptr<pa_threaded_mainloop>* mainloop,
|
||||
// Open playback stream and
|
||||
// tell PulseAudio what the stream icon should be.
|
||||
ScopedPropertyList property_list;
|
||||
+ const std::string cmd_name =
|
||||
+ base::CommandLine::ForCurrentProcess()->GetProgram().BaseName().value();
|
||||
+ const std::string app_id = platform_util::GetXdgAppId().value_or(cmd_name);
|
||||
pa_proplist_sets(property_list.get(), PA_PROP_APPLICATION_ICON_NAME,
|
||||
- kBrowserDisplayName);
|
||||
+ app_id.c_str());
|
||||
*stream = pa_stream_new_with_proplist(
|
||||
*context, "Playback", &sample_specifications, map, property_list.get());
|
||||
RETURN_ON_FAILURE(*stream, "failed to create PA playback stream");
|
||||
@@ -1 +1,2 @@
|
||||
chore_allow_customizing_microtask_policy_per_context.patch
|
||||
build_warn_instead_of_abort_on_builtin_pgo_profile_mismatch.patch
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Sam Attard <sattard@anthropic.com>
|
||||
Date: Sun, 22 Mar 2026 10:51:26 +0000
|
||||
Subject: build: warn instead of abort on builtin PGO profile mismatch
|
||||
|
||||
Electron sets v8_enable_javascript_promise_hooks = true to support
|
||||
Node.js async_hooks (see node/src/env.cc SetPromiseHooks usage:
|
||||
https://github.com/nodejs/node/blob/abff716eaccd0c4f4949d1315cb057a45979649d/src/env.cc#L223-L236).
|
||||
This flag adds conditional branches to builtins-microtask-queue-gen.cc
|
||||
and promise-misc.tq, changing the control-flow graph hash of several
|
||||
Promise/async builtins. This invalidates V8's pre-generated PGO profile
|
||||
for those builtins (built with Chrome defaults where the flag is off).
|
||||
|
||||
Rather than disabling builtins PGO entirely, warn and skip mismatched
|
||||
builtins so all other builtins still benefit from PGO.
|
||||
|
||||
diff --git a/BUILD.gn b/BUILD.gn
|
||||
index 078b63b2bdbb3f952bd0f579e84fb691e308fb64..985f946d5f87b4e6eb32a011ac47a0073248e5f2 100644
|
||||
--- a/BUILD.gn
|
||||
+++ b/BUILD.gn
|
||||
@@ -2803,9 +2803,11 @@ template("run_mksnapshot") {
|
||||
"--turbo-profiling-input",
|
||||
rebase_path(v8_builtins_profiling_log_file, root_build_dir),
|
||||
|
||||
- # Replace this with --warn-about-builtin-profile-data to see the full
|
||||
- # list of builtins with incompatible profiles.
|
||||
- "--abort-on-bad-builtin-profile-data",
|
||||
+ # Electron: Use warn instead of abort so that builtins whose control
|
||||
+ # flow is changed by Electron's build flags (e.g. RunMicrotasks via
|
||||
+ # v8_enable_javascript_promise_hooks) are skipped rather than failing
|
||||
+ # the build. All other builtins still receive PGO.
|
||||
+ "--warn-about-builtin-profile-data",
|
||||
]
|
||||
|
||||
if (!v8_enable_builtins_profiling && v8_enable_builtins_reordering) {
|
||||
@@ -1,22 +1,143 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { parseArgs } from 'node:util';
|
||||
|
||||
import { getChromiumVersionFromDEPS } from './lib/utils.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const ELECTRON_DIR = resolve(__dirname, '..');
|
||||
|
||||
function getCommonTags () {
|
||||
const tags = [];
|
||||
|
||||
if (process.env.TARGET_ARCH) tags.push(`target-arch:${process.env.TARGET_ARCH}`);
|
||||
if (process.env.TARGET_PLATFORM) tags.push(`target-platform:${process.env.TARGET_PLATFORM}`);
|
||||
if (process.env.GITHUB_HEAD_REF) {
|
||||
// Will be set in pull requests
|
||||
tags.push(`branch:${process.env.GITHUB_HEAD_REF}`);
|
||||
} else if (process.env.GITHUB_REF_NAME) {
|
||||
// Will be set for release branches
|
||||
tags.push(`branch:${process.env.GITHUB_REF_NAME}`);
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
async function uploadSeriesToDatadog (series) {
|
||||
await fetch('https://api.datadoghq.com/api/v2/series', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'DD-API-KEY': process.env.DD_API_KEY
|
||||
},
|
||||
body: JSON.stringify({ series })
|
||||
});
|
||||
}
|
||||
|
||||
async function uploadCacheHitRateStats (hitRate, stats) {
|
||||
const timestamp = Math.round(new Date().getTime() / 1000);
|
||||
const tags = getCommonTags();
|
||||
|
||||
const series = [
|
||||
{
|
||||
metric: 'electron.build.effective-cache-hit-rate',
|
||||
points: [{ timestamp, value: (hitRate * 100).toFixed(2) }],
|
||||
type: 3, // GAUGE
|
||||
unit: 'percent',
|
||||
tags
|
||||
}
|
||||
];
|
||||
|
||||
// Add all raw stats as individual metrics
|
||||
for (const [key, value] of Object.entries(stats)) {
|
||||
series.push({
|
||||
metric: `electron.build.stats.${key.toLowerCase()}`,
|
||||
points: [{ timestamp, value }],
|
||||
type: 1, // COUNT
|
||||
tags
|
||||
});
|
||||
}
|
||||
|
||||
await uploadSeriesToDatadog(series);
|
||||
}
|
||||
|
||||
async function uploadObjectChangeStats (stats) {
|
||||
const timestamp = Math.round(new Date().getTime() / 1000);
|
||||
const tags = getCommonTags();
|
||||
|
||||
if (stats['previous-chromium-version']) tags.push(`previous-chromium-version:${stats['previous-chromium-version']}`);
|
||||
if (stats['chromium-version']) tags.push(`chromium-version:${stats['chromium-version']}`);
|
||||
|
||||
if (stats['previous-chromium-version'] && stats['chromium-version']) {
|
||||
tags.push(`chromium-version-changed:${stats['previous-chromium-version'] !== stats['chromium-version']}`);
|
||||
}
|
||||
|
||||
const series = [
|
||||
{
|
||||
metric: 'electron.build.object-change-rate',
|
||||
points: [{ timestamp, value: (stats['change-rate'] * 100).toFixed(2) }],
|
||||
type: 3, // GAUGE
|
||||
unit: 'percent',
|
||||
tags
|
||||
},
|
||||
{
|
||||
metric: 'electron.build.object-change-size',
|
||||
points: [{ timestamp, value: stats['change-size'] }],
|
||||
type: 1, // COUNT
|
||||
unit: 'byte',
|
||||
tags
|
||||
},
|
||||
{
|
||||
metric: 'electron.build.new-object-count',
|
||||
points: [{ timestamp, value: stats['new-object-count'] }],
|
||||
type: 1, // COUNT
|
||||
unit: 'count',
|
||||
tags
|
||||
}
|
||||
];
|
||||
|
||||
await uploadSeriesToDatadog(series);
|
||||
}
|
||||
|
||||
async function main () {
|
||||
const { positionals: [filename], values: { 'upload-stats': uploadStats } } = parseArgs({
|
||||
const { positionals: [filename], values } = parseArgs({
|
||||
allowPositionals: true,
|
||||
options: {
|
||||
'upload-stats': {
|
||||
type: 'boolean',
|
||||
default: false
|
||||
},
|
||||
'out-dir': {
|
||||
type: 'string'
|
||||
},
|
||||
'input-object-checksums': {
|
||||
type: 'string'
|
||||
},
|
||||
'output-object-checksums': {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
'upload-stats': uploadStats,
|
||||
'out-dir': outDir,
|
||||
'input-object-checksums': inputObjectChecksums,
|
||||
'output-object-checksums': outputObjectChecksums
|
||||
} = values;
|
||||
|
||||
if (!filename) {
|
||||
throw new Error('filename is required (should be a siso.INFO file)');
|
||||
}
|
||||
|
||||
if ((inputObjectChecksums || outputObjectChecksums) && !outDir) {
|
||||
throw new Error('--out-dir is required when using --input-object-checksums or --output-object-checksums');
|
||||
} else if (outDir && (!inputObjectChecksums && !outputObjectChecksums)) {
|
||||
throw new Error('--out-dir only makes sense with --input-object-checksums or --output-object-checksums');
|
||||
}
|
||||
|
||||
const log = await fs.readFile(filename, 'utf-8');
|
||||
|
||||
// We expect to find a line which looks like stats=build.Stats{..., CacheHit:39008, Local:4778, Remote:0, LocalFallback:0, ...}
|
||||
@@ -33,55 +154,83 @@ async function main () {
|
||||
const hitRate = stats.CacheHit / (stats.Remote + stats.CacheHit + stats.LocalFallback);
|
||||
|
||||
const messagePrefix = process.env.GITHUB_ACTIONS ? '::notice title=Build Stats::' : '';
|
||||
|
||||
console.log(`${messagePrefix}Effective cache hit rate: ${(hitRate * 100).toFixed(2)}%`);
|
||||
|
||||
const objectChangeStats = {};
|
||||
|
||||
if (inputObjectChecksums || outputObjectChecksums) {
|
||||
const depsContent = await fs.readFile(resolve(ELECTRON_DIR, 'DEPS'), 'utf8');
|
||||
const currentVersion = getChromiumVersionFromDEPS(depsContent);
|
||||
|
||||
// Calculate the SHA256 for each object file under `outDir`
|
||||
const objectFiles = await fs.readdir(outDir, { encoding: 'utf8', recursive: true });
|
||||
const checksums = {};
|
||||
for (const file of objectFiles.filter(f => f.endsWith('.o'))) {
|
||||
const content = await fs.readFile(resolve(outDir, file));
|
||||
checksums[file] = createHash('sha256').update(content).digest('hex');
|
||||
}
|
||||
|
||||
if (outputObjectChecksums) {
|
||||
const outputData = {
|
||||
chromiumVersion: currentVersion,
|
||||
checksums
|
||||
};
|
||||
|
||||
await fs.writeFile(outputObjectChecksums, JSON.stringify(outputData, null, 2));
|
||||
}
|
||||
|
||||
if (inputObjectChecksums) {
|
||||
const inputData = JSON.parse(await fs.readFile(inputObjectChecksums, 'utf8'));
|
||||
const inputFiles = Object.keys(inputData.checksums);
|
||||
let changedCount = 0;
|
||||
let newObjectCount = 0;
|
||||
let changedSize = 0;
|
||||
|
||||
// Count changed files (only those present in both input and current)
|
||||
for (const file of inputFiles) {
|
||||
if (!(file in checksums)) continue; // Skip deleted files
|
||||
if (inputData.checksums[file] !== checksums[file]) {
|
||||
changedCount++;
|
||||
const stat = await fs.stat(resolve(outDir, file));
|
||||
changedSize += stat.size;
|
||||
}
|
||||
}
|
||||
|
||||
// Count new files (in current but not in input)
|
||||
for (const file of Object.keys(checksums)) {
|
||||
if (!(file in inputData.checksums)) {
|
||||
newObjectCount++;
|
||||
const stat = await fs.stat(resolve(outDir, file));
|
||||
changedSize += stat.size;
|
||||
}
|
||||
}
|
||||
|
||||
const changeRate = inputFiles.length > 0 ? changedCount / inputFiles.length : 0;
|
||||
console.log(`${messagePrefix}Object change rate: ${(changeRate * 100).toFixed(2)}%`);
|
||||
if (newObjectCount > 0) {
|
||||
console.log(`${messagePrefix}New object count: ${newObjectCount}`);
|
||||
}
|
||||
console.log(`${messagePrefix}Cumulative changed object sizes: ${changedSize.toLocaleString()} bytes`);
|
||||
|
||||
objectChangeStats['change-rate'] = changeRate;
|
||||
objectChangeStats['change-size'] = changedSize;
|
||||
objectChangeStats['new-object-count'] = newObjectCount;
|
||||
objectChangeStats['previous-chromium-version'] = inputData.chromiumVersion;
|
||||
objectChangeStats['chromium-version'] = currentVersion;
|
||||
}
|
||||
}
|
||||
|
||||
if (uploadStats) {
|
||||
if (!process.env.DD_API_KEY) {
|
||||
throw new Error('DD_API_KEY is not set');
|
||||
}
|
||||
|
||||
const timestamp = Math.round(new Date().getTime() / 1000);
|
||||
await uploadCacheHitRateStats(hitRate, stats);
|
||||
|
||||
const tags = [];
|
||||
|
||||
if (process.env.TARGET_ARCH) tags.push(`target-arch:${process.env.TARGET_ARCH}`);
|
||||
if (process.env.TARGET_PLATFORM) tags.push(`target-platform:${process.env.TARGET_PLATFORM}`);
|
||||
if (process.env.GITHUB_HEAD_REF) {
|
||||
// Will be set in pull requests
|
||||
tags.push(`branch:${process.env.GITHUB_HEAD_REF}`);
|
||||
} else if (process.env.GITHUB_REF_NAME) {
|
||||
// Will be set for release branches
|
||||
tags.push(`branch:${process.env.GITHUB_REF_NAME}`);
|
||||
if (Object.keys(objectChangeStats).length > 0) {
|
||||
await uploadObjectChangeStats(objectChangeStats);
|
||||
}
|
||||
|
||||
const series = [
|
||||
{
|
||||
metric: 'electron.build.effective-cache-hit-rate',
|
||||
points: [{ timestamp, value: (hitRate * 100).toFixed(2) }],
|
||||
type: 3, // GAUGE
|
||||
unit: 'percent',
|
||||
tags
|
||||
}
|
||||
];
|
||||
|
||||
// Add all raw stats as individual metrics
|
||||
for (const [key, value] of Object.entries(stats)) {
|
||||
series.push({
|
||||
metric: `electron.build.stats.${key.toLowerCase()}`,
|
||||
points: [{ timestamp, value }],
|
||||
type: 1, // COUNT
|
||||
tags
|
||||
});
|
||||
}
|
||||
|
||||
await fetch('https://api.datadoghq.com/api/v2/series', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'DD-API-KEY': process.env.DD_API_KEY
|
||||
},
|
||||
body: JSON.stringify({ series })
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ const path = require('node:path');
|
||||
const ELECTRON_DIR = path.resolve(__dirname, '..', '..');
|
||||
const SRC_DIR = path.resolve(ELECTRON_DIR, '..');
|
||||
|
||||
const CHROMIUM_VERSION_DEPS_REGEX = /chromium_version':\n +'(.+?)',/m;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const pass = chalk.green('✓');
|
||||
const fail = chalk.red('✗');
|
||||
@@ -162,10 +164,15 @@ function compareVersions (v1, v2) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getChromiumVersionFromDEPS (depsContent) {
|
||||
return CHROMIUM_VERSION_DEPS_REGEX.exec(depsContent)?.[1] ?? null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
chunkFilenames,
|
||||
compareVersions,
|
||||
findMatchingFiles,
|
||||
getChromiumVersionFromDEPS,
|
||||
getCurrentBranch,
|
||||
getElectronExec,
|
||||
getOutDir,
|
||||
|
||||
@@ -5,12 +5,11 @@ import * as fs from 'node:fs/promises';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { compareVersions } from './lib/utils.js';
|
||||
import { compareVersions, getChromiumVersionFromDEPS } from './lib/utils.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const ELECTRON_DIR = resolve(__dirname, '..');
|
||||
|
||||
const DEPS_REGEX = /chromium_version':\n +'(.+?)',/m;
|
||||
const CL_REGEX = /https:\/\/chromium-review\.googlesource\.com\/c\/(?:chromium\/src|v8\/v8)\/\+\/(\d+)(#\S+)?/g;
|
||||
const ROLLER_BRANCH_PATTERN = /^roller\/chromium\/(.+)$/;
|
||||
|
||||
@@ -140,12 +139,12 @@ async function main () {
|
||||
cwd: ELECTRON_DIR,
|
||||
encoding: 'utf8'
|
||||
});
|
||||
baseVersion = DEPS_REGEX.exec(baseDepsContent)?.[1] ?? null;
|
||||
baseVersion = getChromiumVersionFromDEPS(baseDepsContent);
|
||||
} catch {
|
||||
// baseVersion remains null
|
||||
}
|
||||
const depsContent = await fs.readFile(resolve(ELECTRON_DIR, 'DEPS'), 'utf8');
|
||||
const newVersion = DEPS_REGEX.exec(depsContent)?.[1] ?? null;
|
||||
const newVersion = getChromiumVersionFromDEPS(depsContent);
|
||||
|
||||
if (!baseVersion || !newVersion) {
|
||||
console.error('Could not determine Chromium version range');
|
||||
|
||||
@@ -10,15 +10,20 @@
|
||||
#include <vector>
|
||||
|
||||
#include "base/task/single_thread_task_runner.h"
|
||||
#include "components/prefs/scoped_user_pref_update.h"
|
||||
#include "content/public/common/color_parser.h"
|
||||
#include "electron/buildflags/buildflags.h"
|
||||
#include "gin/dictionary.h"
|
||||
#include "shell/browser/api/electron_api_menu.h"
|
||||
#include "shell/browser/api/electron_api_view.h"
|
||||
#include "shell/browser/api/electron_api_web_contents.h"
|
||||
#include "shell/browser/browser_process_impl.h"
|
||||
#include "shell/browser/electron_browser_main_parts.h"
|
||||
#include "shell/browser/javascript_environment.h"
|
||||
#include "shell/browser/native_window.h"
|
||||
#include "shell/browser/window_list.h"
|
||||
#include "shell/common/color_util.h"
|
||||
#include "shell/common/electron_constants.h"
|
||||
#include "shell/common/gin_converters/callback_converter.h"
|
||||
#include "shell/common/gin_converters/file_path_converter.h"
|
||||
#include "shell/common/gin_converters/gfx_converter.h"
|
||||
@@ -170,7 +175,7 @@ void BaseWindow::OnWindowClosed() {
|
||||
// We can not call Destroy here because we need to call Emit first, but we
|
||||
// also do not want any method to be used, so just mark as destroyed here.
|
||||
MarkDestroyed();
|
||||
|
||||
window_->FlushWindowState();
|
||||
Emit("closed");
|
||||
|
||||
parent_window_.Reset();
|
||||
@@ -261,6 +266,7 @@ void BaseWindow::OnWindowWillResize(const gfx::Rect& new_bounds,
|
||||
}
|
||||
|
||||
void BaseWindow::OnWindowResize() {
|
||||
window_->DebouncedSaveWindowState();
|
||||
Emit("resize");
|
||||
}
|
||||
|
||||
@@ -276,6 +282,7 @@ void BaseWindow::OnWindowWillMove(const gfx::Rect& new_bounds,
|
||||
}
|
||||
|
||||
void BaseWindow::OnWindowMove() {
|
||||
window_->DebouncedSaveWindowState();
|
||||
Emit("move");
|
||||
}
|
||||
|
||||
@@ -344,6 +351,10 @@ void BaseWindow::OnSystemContextMenu(int x, int y, bool* prevent_default) {
|
||||
}
|
||||
}
|
||||
|
||||
void BaseWindow::OnWindowStateRestored() {
|
||||
EmitEventSoon("restored-persisted-state");
|
||||
}
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
void BaseWindow::OnWindowMessage(UINT message, WPARAM w_param, LPARAM l_param) {
|
||||
if (IsWindowMessageHooked(message)) {
|
||||
@@ -1152,14 +1163,64 @@ void BaseWindow::SetTitleBarOverlay(const gin_helper::Dictionary& options,
|
||||
}
|
||||
#endif
|
||||
|
||||
// static
|
||||
void BaseWindow::ClearPersistedState(const std::string& window_name) {
|
||||
if (window_name.empty()) {
|
||||
LOG(WARNING) << "Cannot clear persisted window state: window name is empty";
|
||||
return;
|
||||
}
|
||||
|
||||
if (auto* browser_process =
|
||||
electron::ElectronBrowserMainParts::Get()->browser_process()) {
|
||||
DCHECK(browser_process);
|
||||
if (auto* prefs = browser_process->local_state()) {
|
||||
ScopedDictPrefUpdate update(prefs, electron::kWindowStates);
|
||||
|
||||
if (!update->Remove(window_name)) {
|
||||
LOG(WARNING) << "Window state '" << window_name
|
||||
<< "' not found, nothing to clear";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// static
|
||||
gin_helper::WrappableBase* BaseWindow::New(gin::Arguments* const args) {
|
||||
auto options = gin_helper::Dictionary::CreateEmpty(args->isolate());
|
||||
args->GetNext(&options);
|
||||
|
||||
std::string error_message;
|
||||
if (!IsWindowNameValid(options, &error_message)) {
|
||||
// Window name is already in use throw an error and do not create the window
|
||||
args->ThrowTypeError(error_message);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return new BaseWindow(args, options);
|
||||
}
|
||||
|
||||
// static
|
||||
bool BaseWindow::IsWindowNameValid(const gin_helper::Dictionary& options,
|
||||
std::string* error_message) {
|
||||
std::string window_name;
|
||||
if (options.Get(options::kName, &window_name) && !window_name.empty()) {
|
||||
// Check if window name is already in use by another window
|
||||
// Window names must be unique for state persistence to work correctly
|
||||
const auto& windows = electron::WindowList::GetWindows();
|
||||
bool name_in_use = std::any_of(windows.begin(), windows.end(),
|
||||
[&window_name](const auto* const window) {
|
||||
return window->GetName() == window_name;
|
||||
});
|
||||
|
||||
if (name_in_use) {
|
||||
*error_message = "Window name '" + window_name +
|
||||
"' is already in use. Window names must be unique.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// static
|
||||
void BaseWindow::BuildPrototype(v8::Isolate* isolate,
|
||||
v8::Local<v8::FunctionTemplate> prototype) {
|
||||
@@ -1351,6 +1412,8 @@ void Initialize(v8::Local<v8::Object> exports,
|
||||
.ToLocalChecked());
|
||||
constructor.SetMethod("fromId", &BaseWindow::FromWeakMapID);
|
||||
constructor.SetMethod("getAllWindows", &BaseWindow::GetAll);
|
||||
constructor.SetMethod("clearPersistedState",
|
||||
&BaseWindow::ClearPersistedState);
|
||||
|
||||
gin_helper::Dictionary dict(isolate, exports);
|
||||
dict.Set("BaseWindow", constructor);
|
||||
|
||||
@@ -44,6 +44,13 @@ class BaseWindow : public gin_helper::TrackableObject<BaseWindow>,
|
||||
static void BuildPrototype(v8::Isolate* isolate,
|
||||
v8::Local<v8::FunctionTemplate> prototype);
|
||||
|
||||
// Clears window state from the Local State JSON file in
|
||||
// app.getPath('userData') via PrefService.
|
||||
static void ClearPersistedState(const std::string& window_name);
|
||||
|
||||
static bool IsWindowNameValid(const gin_helper::Dictionary& options,
|
||||
std::string* error_message);
|
||||
|
||||
const NativeWindow* window() const { return window_.get(); }
|
||||
NativeWindow* window() { return window_.get(); }
|
||||
|
||||
@@ -95,6 +102,7 @@ class BaseWindow : public gin_helper::TrackableObject<BaseWindow>,
|
||||
const base::DictValue& details) override;
|
||||
void OnNewWindowForTab() override;
|
||||
void OnSystemContextMenu(int x, int y, bool* prevent_default) override;
|
||||
void OnWindowStateRestored() override;
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
void OnWindowMessage(UINT message, WPARAM w_param, LPARAM l_param) override;
|
||||
#endif
|
||||
|
||||
@@ -322,6 +322,13 @@ gin_helper::WrappableBase* BrowserWindow::New(gin_helper::ErrorThrower thrower,
|
||||
options = gin::Dictionary::CreateEmpty(args->isolate());
|
||||
}
|
||||
|
||||
std::string error_message;
|
||||
if (!IsWindowNameValid(options, &error_message)) {
|
||||
// Window name is already in use throw an error and do not create the window
|
||||
thrower.ThrowError(error_message);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return new BrowserWindow(args, options);
|
||||
}
|
||||
|
||||
|
||||
@@ -54,17 +54,9 @@ gin_helper::Handle<SafeStorage> SafeStorage::Create(v8::Isolate* isolate) {
|
||||
return gin_helper::CreateHandle(isolate, new SafeStorage(isolate));
|
||||
}
|
||||
|
||||
SafeStorage::SafeStorage(v8::Isolate* isolate) {
|
||||
if (electron::Browser::Get()->is_ready()) {
|
||||
OnFinishLaunching({});
|
||||
} else {
|
||||
Browser::Get()->AddObserver(this);
|
||||
}
|
||||
}
|
||||
SafeStorage::SafeStorage(v8::Isolate* isolate) {}
|
||||
|
||||
SafeStorage::~SafeStorage() {
|
||||
Browser::Get()->RemoveObserver(this);
|
||||
}
|
||||
SafeStorage::~SafeStorage() = default;
|
||||
|
||||
gin::ObjectTemplateBuilder SafeStorage::GetObjectTemplateBuilder(
|
||||
v8::Isolate* isolate) {
|
||||
@@ -85,7 +77,11 @@ gin::ObjectTemplateBuilder SafeStorage::GetObjectTemplateBuilder(
|
||||
;
|
||||
}
|
||||
|
||||
void SafeStorage::OnFinishLaunching(base::DictValue launch_info) {
|
||||
void SafeStorage::EnsureAsyncEncryptorRequested() {
|
||||
DCHECK(electron::Browser::Get()->is_ready());
|
||||
if (encryptor_requested_)
|
||||
return;
|
||||
encryptor_requested_ = true;
|
||||
g_browser_process->os_crypt_async()->GetInstance(
|
||||
base::BindOnce(&SafeStorage::OnOsCryptReady, base::Unretained(this)),
|
||||
os_crypt_async::Encryptor::Option::kEncryptSyncCompat);
|
||||
@@ -95,13 +91,21 @@ void SafeStorage::OnOsCryptReady(os_crypt_async::Encryptor encryptor) {
|
||||
encryptor_ = std::move(encryptor);
|
||||
is_available_ = true;
|
||||
|
||||
// This callback may fire from a posted task without an active V8 scope.
|
||||
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
|
||||
v8::HandleScope handle_scope(isolate);
|
||||
|
||||
for (auto& pending : pending_availability_checks_) {
|
||||
pending.Resolve(true);
|
||||
}
|
||||
pending_availability_checks_.clear();
|
||||
|
||||
for (auto& pending : pending_encrypts_) {
|
||||
std::string ciphertext;
|
||||
bool encrypted = encryptor_->EncryptString(pending.plaintext, &ciphertext);
|
||||
if (encrypted) {
|
||||
pending.promise.Resolve(
|
||||
electron::Buffer::Copy(pending.promise.isolate(), ciphertext)
|
||||
.ToLocalChecked());
|
||||
electron::Buffer::Copy(isolate, ciphertext).ToLocalChecked());
|
||||
} else {
|
||||
pending.promise.RejectWithErrorMessage(
|
||||
"Error while encrypting the text provided to "
|
||||
@@ -117,8 +121,6 @@ void SafeStorage::OnOsCryptReady(os_crypt_async::Encryptor encryptor) {
|
||||
encryptor_->DecryptString(pending.ciphertext, &plaintext, &flags);
|
||||
|
||||
if (decrypted) {
|
||||
v8::Isolate* isolate = pending.promise.isolate();
|
||||
v8::HandleScope handle_scope(isolate);
|
||||
auto dict = gin_helper::Dictionary::CreateEmpty(isolate);
|
||||
|
||||
dict.Set("shouldReEncrypt", flags.should_reencrypt);
|
||||
@@ -155,16 +157,33 @@ bool SafeStorage::IsEncryptionAvailable() {
|
||||
#endif
|
||||
}
|
||||
|
||||
bool SafeStorage::IsAsyncEncryptionAvailable() {
|
||||
if (!electron::Browser::Get()->is_ready())
|
||||
return false;
|
||||
v8::Local<v8::Promise> SafeStorage::IsAsyncEncryptionAvailable(
|
||||
v8::Isolate* isolate) {
|
||||
gin_helper::Promise<bool> promise(isolate);
|
||||
v8::Local<v8::Promise> handle = promise.GetHandle();
|
||||
|
||||
if (!electron::Browser::Get()->is_ready()) {
|
||||
promise.Resolve(false);
|
||||
return handle;
|
||||
}
|
||||
|
||||
#if BUILDFLAG(IS_LINUX)
|
||||
return is_available_ || (use_password_v10_ &&
|
||||
static_cast<BrowserProcessImpl*>(g_browser_process)
|
||||
->linux_storage_backend() == "basic_text");
|
||||
#else
|
||||
return is_available_;
|
||||
if (use_password_v10_ && static_cast<BrowserProcessImpl*>(g_browser_process)
|
||||
->linux_storage_backend() == "basic_text") {
|
||||
promise.Resolve(true);
|
||||
return handle;
|
||||
}
|
||||
#endif
|
||||
|
||||
EnsureAsyncEncryptorRequested();
|
||||
|
||||
if (is_available_) {
|
||||
promise.Resolve(true);
|
||||
return handle;
|
||||
}
|
||||
|
||||
pending_availability_checks_.push_back(std::move(promise));
|
||||
return handle;
|
||||
}
|
||||
|
||||
void SafeStorage::SetUsePasswordV10(bool use) {
|
||||
@@ -270,6 +289,8 @@ v8::Local<v8::Promise> SafeStorage::encryptStringAsync(
|
||||
return handle;
|
||||
}
|
||||
|
||||
EnsureAsyncEncryptorRequested();
|
||||
|
||||
if (is_available_) {
|
||||
std::string ciphertext;
|
||||
bool encrypted = encryptor_->EncryptString(plaintext, &ciphertext);
|
||||
@@ -318,6 +339,8 @@ v8::Local<v8::Promise> SafeStorage::decryptStringAsync(
|
||||
return handle;
|
||||
}
|
||||
|
||||
EnsureAsyncEncryptorRequested();
|
||||
|
||||
if (is_available_) {
|
||||
std::string plaintext;
|
||||
os_crypt_async::Encryptor::DecryptFlags flags;
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
|
||||
#include "build/build_config.h"
|
||||
#include "components/os_crypt/async/common/encryptor.h"
|
||||
#include "shell/browser/browser_observer.h"
|
||||
#include "shell/browser/event_emitter_mixin.h"
|
||||
#include "shell/common/gin_helper/dictionary.h"
|
||||
#include "shell/common/gin_helper/promise.h"
|
||||
@@ -37,8 +36,7 @@ class Handle;
|
||||
namespace electron::api {
|
||||
|
||||
class SafeStorage final : public gin_helper::DeprecatedWrappable<SafeStorage>,
|
||||
public gin_helper::EventEmitterMixin<SafeStorage>,
|
||||
private BrowserObserver {
|
||||
public gin_helper::EventEmitterMixin<SafeStorage> {
|
||||
public:
|
||||
static gin_helper::Handle<SafeStorage> Create(v8::Isolate* isolate);
|
||||
|
||||
@@ -57,14 +55,16 @@ class SafeStorage final : public gin_helper::DeprecatedWrappable<SafeStorage>,
|
||||
~SafeStorage() override;
|
||||
|
||||
private:
|
||||
// BrowserObserver:
|
||||
void OnFinishLaunching(base::DictValue launch_info) override;
|
||||
// Lazily request the async encryptor on first use. ESM named imports
|
||||
// eagerly evaluate all electron module getters, so requesting in the
|
||||
// constructor would touch the OS keychain even when safeStorage is unused.
|
||||
void EnsureAsyncEncryptorRequested();
|
||||
|
||||
void OnOsCryptReady(os_crypt_async::Encryptor encryptor);
|
||||
|
||||
bool IsEncryptionAvailable();
|
||||
|
||||
bool IsAsyncEncryptionAvailable();
|
||||
v8::Local<v8::Promise> IsAsyncEncryptionAvailable(v8::Isolate* isolate);
|
||||
|
||||
void SetUsePasswordV10(bool use);
|
||||
|
||||
@@ -85,6 +85,7 @@ class SafeStorage final : public gin_helper::DeprecatedWrappable<SafeStorage>,
|
||||
|
||||
bool use_password_v10_ = false;
|
||||
|
||||
bool encryptor_requested_ = false;
|
||||
bool is_available_ = false;
|
||||
|
||||
std::optional<os_crypt_async::Encryptor> encryptor_;
|
||||
@@ -114,6 +115,8 @@ class SafeStorage final : public gin_helper::DeprecatedWrappable<SafeStorage>,
|
||||
std::string ciphertext;
|
||||
};
|
||||
std::vector<PendingDecrypt> pending_decrypts_;
|
||||
|
||||
std::vector<gin_helper::Promise<bool>> pending_availability_checks_;
|
||||
};
|
||||
|
||||
} // namespace electron::api
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
#include "services/device/public/cpp/geolocation/geolocation_system_permission_manager.h"
|
||||
#include "services/network/public/cpp/network_switches.h"
|
||||
#include "shell/browser/net/resolve_proxy_helper.h"
|
||||
#include "shell/common/electron_constants.h"
|
||||
#include "shell/common/electron_paths.h"
|
||||
#include "shell/common/thread_restrictions.h"
|
||||
|
||||
@@ -149,12 +150,12 @@ void BrowserProcessImpl::PostEarlyInitialization() {
|
||||
pref_registry.get());
|
||||
#endif
|
||||
|
||||
pref_registry->RegisterDictionaryPref(electron::kWindowStates);
|
||||
|
||||
in_memory_pref_store_ = base::MakeRefCounted<ValueMapPrefStore>();
|
||||
ApplyProxyModeFromCommandLine(in_memory_pref_store());
|
||||
prefs_factory.set_command_line_prefs(in_memory_pref_store());
|
||||
|
||||
// Only use a persistent prefs store when cookie encryption is enabled as that
|
||||
// is the only key that needs it
|
||||
base::FilePath prefs_path;
|
||||
CHECK(base::PathService::Get(electron::DIR_SESSION_DATA, &prefs_path));
|
||||
prefs_path = prefs_path.Append(FILE_PATH_LITERAL("Local State"));
|
||||
|
||||
@@ -127,6 +127,10 @@
|
||||
#include "shell/common/plugin_info.h"
|
||||
#endif // BUILDFLAG(ENABLE_PLUGINS)
|
||||
|
||||
#if BUILDFLAG(ENABLE_PRINTING)
|
||||
#include "components/printing/common/print_dialog_linux_factory.h"
|
||||
#endif
|
||||
|
||||
namespace electron {
|
||||
|
||||
namespace {
|
||||
@@ -415,6 +419,10 @@ void ElectronBrowserMainParts::ToolkitInitialized() {
|
||||
|
||||
ui::LinuxUi::SetInstance(linux_ui);
|
||||
|
||||
#if BUILDFLAG(ENABLE_PRINTING)
|
||||
print_dialog_factory_ = std::make_unique<printing::PrintDialogLinuxFactory>();
|
||||
#endif
|
||||
|
||||
// Cursor theme changes are tracked by LinuxUI (via a CursorThemeManager
|
||||
// implementation). Start observing them once it's initialized.
|
||||
ui::CursorFactory::GetInstance()->ObserveThemeChanges();
|
||||
|
||||
@@ -14,8 +14,13 @@
|
||||
#include "content/public/browser/browser_main_parts.h"
|
||||
#include "electron/buildflags/buildflags.h"
|
||||
#include "mojo/public/cpp/bindings/remote.h"
|
||||
#include "printing/buildflags/buildflags.h"
|
||||
#include "services/device/public/mojom/geolocation_control.mojom.h"
|
||||
|
||||
#if BUILDFLAG(ENABLE_PRINTING)
|
||||
#include "printing/printing_context_linux.h"
|
||||
#endif
|
||||
|
||||
class BrowserProcessImpl;
|
||||
class IconManager;
|
||||
|
||||
@@ -179,6 +184,11 @@ class ElectronBrowserMainParts : public content::BrowserMainParts {
|
||||
std::unique_ptr<display::ScopedNativeScreen> screen_;
|
||||
#endif
|
||||
|
||||
#if BUILDFLAG(ENABLE_PRINTING)
|
||||
std::unique_ptr<printing::PrintingContextLinux::PrintDialogFactory>
|
||||
print_dialog_factory_;
|
||||
#endif
|
||||
|
||||
static ElectronBrowserMainParts* self_;
|
||||
};
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ v8::Isolate* JavascriptEnvironment::Initialize(uv_loop_t* event_loop,
|
||||
node::tracing::TraceEventHelper::SetAgent(tracing_agent);
|
||||
platform_ = node::MultiIsolatePlatform::Create(
|
||||
base::RecommendedMaxNumberOfThreadsInThreadGroup(3, 8, 0.1, 0),
|
||||
tracing_controller, gin::V8Platform::GetCurrentPageAllocator());
|
||||
tracing_controller, gin::V8Platform::Get()->GetPageAllocator());
|
||||
|
||||
v8::V8::InitializePlatform(platform_.get());
|
||||
gin::IsolateHolder::Initialize(
|
||||
|
||||
@@ -9,22 +9,31 @@
|
||||
#include <vector>
|
||||
|
||||
#include "base/memory/ptr_util.h"
|
||||
#include "base/memory/raw_ptr.h"
|
||||
#include "base/strings/utf_string_conversions.h"
|
||||
#include "base/values.h"
|
||||
#include "components/prefs/pref_service.h"
|
||||
#include "components/prefs/scoped_user_pref_update.h"
|
||||
#include "content/public/browser/web_contents_user_data.h"
|
||||
#include "include/core/SkColor.h"
|
||||
#include "shell/browser/background_throttling_source.h"
|
||||
#include "shell/browser/browser.h"
|
||||
#include "shell/browser/browser_process_impl.h"
|
||||
#include "shell/browser/draggable_region_provider.h"
|
||||
#include "shell/browser/electron_browser_main_parts.h"
|
||||
#include "shell/browser/native_window_features.h"
|
||||
#include "shell/browser/ui/drag_util.h"
|
||||
#include "shell/browser/window_list.h"
|
||||
#include "shell/common/color_util.h"
|
||||
#include "shell/common/electron_constants.h"
|
||||
#include "shell/common/gin_helper/dictionary.h"
|
||||
#include "shell/common/gin_helper/persistent_dictionary.h"
|
||||
#include "shell/common/options_switches.h"
|
||||
#include "ui/base/hit_test.h"
|
||||
#include "ui/compositor/compositor.h"
|
||||
#include "ui/display/display.h"
|
||||
#include "ui/display/screen.h"
|
||||
#include "ui/display/types/display_constants.h"
|
||||
#include "ui/views/widget/widget.h"
|
||||
|
||||
#if !BUILDFLAG(IS_MAC)
|
||||
@@ -94,6 +103,12 @@ gfx::Size GetExpandedWindowSize(const NativeWindow* window,
|
||||
}
|
||||
#endif
|
||||
|
||||
// Check if display is fake (default display ID) or has invalid dimensions
|
||||
bool hasInvalidDisplay(const display::Display& display) {
|
||||
return display.id() == display::kDefaultDisplayId ||
|
||||
display.size().width() == 0 || display.size().height() == 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
NativeWindow::NativeWindow(const int32_t base_window_id,
|
||||
@@ -118,6 +133,38 @@ NativeWindow::NativeWindow(const int32_t base_window_id,
|
||||
options.Get(options::kVibrancyType, &vibrancy_);
|
||||
#endif
|
||||
|
||||
options.Get(options::kName, &window_name_);
|
||||
|
||||
if (gin_helper::Dictionary persistence_options;
|
||||
options.Get(options::kWindowStatePersistence, &persistence_options)) {
|
||||
// Restore bounds by default
|
||||
restore_bounds_ = true;
|
||||
persistence_options.Get(options::kBounds, &restore_bounds_);
|
||||
// Restore display mode by default
|
||||
restore_display_mode_ = true;
|
||||
persistence_options.Get(options::kDisplayMode, &restore_display_mode_);
|
||||
window_state_persistence_enabled_ = true;
|
||||
} else if (bool flag; options.Get(options::kWindowStatePersistence, &flag)) {
|
||||
restore_bounds_ = flag;
|
||||
restore_display_mode_ = flag;
|
||||
window_state_persistence_enabled_ = flag;
|
||||
}
|
||||
|
||||
// Initialize prefs_ to save/restore window bounds if we have a valid window
|
||||
// name and window state persistence is enabled.
|
||||
if (window_state_persistence_enabled_ && !window_name_.empty()) {
|
||||
// Move this out if there's a need to initialize prefs_ for other features
|
||||
if (auto* browser_process =
|
||||
electron::ElectronBrowserMainParts::Get()->browser_process()) {
|
||||
DCHECK(browser_process);
|
||||
prefs_ = browser_process->local_state();
|
||||
}
|
||||
} else if (window_state_persistence_enabled_ && window_name_.empty()) {
|
||||
window_state_persistence_enabled_ = false;
|
||||
LOG(WARNING) << "Window state persistence enabled but no window name "
|
||||
"provided. Window state will not be persisted.";
|
||||
}
|
||||
|
||||
if (gin_helper::Dictionary dict;
|
||||
options.Get(options::ktitleBarOverlay, &dict)) {
|
||||
titlebar_overlay_ = true;
|
||||
@@ -216,7 +263,14 @@ void NativeWindow::InitFromOptions(const gin_helper::Dictionary& options) {
|
||||
options.Get(options::kFullScreenable, &fullscreenable);
|
||||
SetFullScreenable(fullscreenable);
|
||||
|
||||
if (fullscreen)
|
||||
// Restore window state (bounds and display mode) at this point in
|
||||
// initialization. We deliberately restore bounds before display modes
|
||||
// (fullscreen/kiosk) since the target display for these states depends on the
|
||||
// window's initial bounds. Also, restoring here ensures we respect min/max
|
||||
// width/height and fullscreenable constraints.
|
||||
RestoreWindowState(options);
|
||||
|
||||
if (fullscreen && !restore_display_mode_)
|
||||
SetFullScreen(true);
|
||||
|
||||
if (bool val; options.Get(options::kResizable, &val))
|
||||
@@ -225,7 +279,8 @@ void NativeWindow::InitFromOptions(const gin_helper::Dictionary& options) {
|
||||
if (bool val; options.Get(options::kSkipTaskbar, &val))
|
||||
SetSkipTaskbar(val);
|
||||
|
||||
if (bool val; options.Get(options::kKiosk, &val) && val)
|
||||
if (bool val;
|
||||
options.Get(options::kKiosk, &val) && val && !restore_display_mode_)
|
||||
SetKiosk(val);
|
||||
|
||||
#if BUILDFLAG(IS_MAC)
|
||||
@@ -245,7 +300,9 @@ void NativeWindow::InitFromOptions(const gin_helper::Dictionary& options) {
|
||||
SetBackgroundColor(background_color);
|
||||
|
||||
SetTitle(options.ValueOrDefault(options::kTitle, Browser::Get()->GetName()));
|
||||
|
||||
// Save updated window state after restoration adjustments are complete if
|
||||
// any.
|
||||
SaveWindowState();
|
||||
// Then show it.
|
||||
if (options.ValueOrDefault(options::kShow, true))
|
||||
Show();
|
||||
@@ -658,6 +715,10 @@ void NativeWindow::NotifyLayoutWindowControlsOverlay() {
|
||||
*bounds);
|
||||
}
|
||||
|
||||
void NativeWindow::NotifyWindowStateRestored() {
|
||||
observers_.Notify(&NativeWindowObserver::OnWindowStateRestored);
|
||||
}
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
void NativeWindow::NotifyWindowMessage(UINT message,
|
||||
WPARAM w_param,
|
||||
@@ -760,10 +821,14 @@ void NativeWindow::SetAccessibleTitle(const std::string& title) {
|
||||
WidgetDelegate::SetAccessibleTitle(base::UTF8ToUTF16(title));
|
||||
}
|
||||
|
||||
std::string NativeWindow::GetAccessibleTitle() {
|
||||
std::string NativeWindow::GetAccessibleTitle() const {
|
||||
return base::UTF16ToUTF8(GetAccessibleWindowTitle());
|
||||
}
|
||||
|
||||
std::string NativeWindow::GetName() const {
|
||||
return window_name_;
|
||||
}
|
||||
|
||||
void NativeWindow::HandlePendingFullscreenTransitions() {
|
||||
if (pending_transitions_.empty()) {
|
||||
set_fullscreen_transition_type(FullScreenTransitionType::kNone);
|
||||
@@ -796,6 +861,252 @@ bool NativeWindow::IsTranslucent() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
void NativeWindow::DebouncedSaveWindowState() {
|
||||
save_window_state_timer_.Start(
|
||||
FROM_HERE, base::Milliseconds(200),
|
||||
base::BindOnce(&NativeWindow::SaveWindowState, base::Unretained(this)));
|
||||
}
|
||||
|
||||
void NativeWindow::SaveWindowState() {
|
||||
if (!window_state_persistence_enabled_ || is_being_restored_)
|
||||
return;
|
||||
|
||||
gfx::Rect bounds = GetBounds();
|
||||
|
||||
if (bounds.width() == 0 || bounds.height() == 0) {
|
||||
LOG(WARNING) << "Window state not saved - window bounds are invalid";
|
||||
return;
|
||||
}
|
||||
|
||||
const display::Screen* screen = display::Screen::Get();
|
||||
DCHECK(screen);
|
||||
// GetDisplayMatching returns a fake display with 1920x1080 resolution at
|
||||
// (0,0) when no physical displays are attached.
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:ui/display/display.cc;l=184;drc=e4f1aef5f3ec30a28950d766612cc2c04c822c71
|
||||
const display::Display display = screen->GetDisplayMatching(bounds);
|
||||
|
||||
// Skip window state persistence when display has invalid dimensions (0x0) or
|
||||
// is fake (ID 0xFF). Invalid displays could cause incorrect window bounds to
|
||||
// be saved, leading to positioning issues during restoration.
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:ui/display/types/display_constants.h;l=28;drc=e4f1aef5f3ec30a28950d766612cc2c04c822c71
|
||||
if (hasInvalidDisplay(display)) {
|
||||
LOG(WARNING)
|
||||
<< "Window state not saved - no physical display attached or current "
|
||||
"display has invalid bounds";
|
||||
return;
|
||||
}
|
||||
|
||||
ScopedDictPrefUpdate update(prefs_, electron::kWindowStates);
|
||||
const base::Value::Dict* existing_prefs = update->FindDict(window_name_);
|
||||
|
||||
// When the window is in a special display mode (fullscreen, kiosk, or
|
||||
// maximized), save the previously stored window bounds instead of
|
||||
// the current bounds. This ensures that when the window is restored, it can
|
||||
// be restored to its original position and size if display mode is not
|
||||
// preserved via windowStatePersistence.
|
||||
if (!IsNormal() && existing_prefs) {
|
||||
std::optional<int> left = existing_prefs->FindInt(electron::kLeft);
|
||||
std::optional<int> top = existing_prefs->FindInt(electron::kTop);
|
||||
std::optional<int> right = existing_prefs->FindInt(electron::kRight);
|
||||
std::optional<int> bottom = existing_prefs->FindInt(electron::kBottom);
|
||||
|
||||
if (left && top && right && bottom) {
|
||||
bounds = gfx::Rect(*left, *top, *right - *left, *bottom - *top);
|
||||
}
|
||||
}
|
||||
|
||||
base::Value::Dict window_preferences;
|
||||
window_preferences.Set(electron::kLeft, bounds.x());
|
||||
window_preferences.Set(electron::kTop, bounds.y());
|
||||
window_preferences.Set(electron::kRight, bounds.right());
|
||||
window_preferences.Set(electron::kBottom, bounds.bottom());
|
||||
|
||||
window_preferences.Set(electron::kMaximized, IsMaximized());
|
||||
window_preferences.Set(electron::kFullscreen, IsFullscreen());
|
||||
window_preferences.Set(electron::kKiosk, IsKiosk());
|
||||
|
||||
gfx::Rect work_area = display.work_area();
|
||||
|
||||
window_preferences.Set(electron::kWorkAreaLeft, work_area.x());
|
||||
window_preferences.Set(electron::kWorkAreaTop, work_area.y());
|
||||
window_preferences.Set(electron::kWorkAreaRight, work_area.right());
|
||||
window_preferences.Set(electron::kWorkAreaBottom, work_area.bottom());
|
||||
|
||||
update->Set(window_name_, std::move(window_preferences));
|
||||
}
|
||||
|
||||
void NativeWindow::FlushWindowState() {
|
||||
if (save_window_state_timer_.IsRunning()) {
|
||||
save_window_state_timer_.FireNow();
|
||||
} else {
|
||||
SaveWindowState();
|
||||
}
|
||||
}
|
||||
|
||||
void NativeWindow::RestoreWindowState(const gin_helper::Dictionary& options) {
|
||||
if (!window_state_persistence_enabled_)
|
||||
return;
|
||||
|
||||
const base::Value& value = prefs_->GetValue(electron::kWindowStates);
|
||||
const base::Value::Dict* window_preferences =
|
||||
value.is_dict() ? value.GetDict().FindDict(window_name_) : nullptr;
|
||||
|
||||
if (!window_preferences)
|
||||
return;
|
||||
|
||||
std::optional<int> saved_left = window_preferences->FindInt(electron::kLeft);
|
||||
std::optional<int> saved_top = window_preferences->FindInt(electron::kTop);
|
||||
std::optional<int> saved_right =
|
||||
window_preferences->FindInt(electron::kRight);
|
||||
std::optional<int> saved_bottom =
|
||||
window_preferences->FindInt(electron::kBottom);
|
||||
|
||||
std::optional<int> work_area_left =
|
||||
window_preferences->FindInt(electron::kWorkAreaLeft);
|
||||
std::optional<int> work_area_top =
|
||||
window_preferences->FindInt(electron::kWorkAreaTop);
|
||||
std::optional<int> work_area_right =
|
||||
window_preferences->FindInt(electron::kWorkAreaRight);
|
||||
std::optional<int> work_area_bottom =
|
||||
window_preferences->FindInt(electron::kWorkAreaBottom);
|
||||
|
||||
if (!saved_left || !saved_top || !saved_right || !saved_bottom ||
|
||||
!work_area_left || !work_area_top || !work_area_right ||
|
||||
!work_area_bottom) {
|
||||
LOG(WARNING) << "Window state not restored - corrupted values found";
|
||||
return;
|
||||
}
|
||||
|
||||
gfx::Rect saved_bounds =
|
||||
gfx::Rect(*saved_left, *saved_top, *saved_right - *saved_left,
|
||||
*saved_bottom - *saved_top);
|
||||
|
||||
display::Screen* screen = display::Screen::Get();
|
||||
DCHECK(screen);
|
||||
|
||||
// Set the primary display as the target display for restoration.
|
||||
display::Display display = screen->GetPrimaryDisplay();
|
||||
|
||||
// We identify the display with the minimal Manhattan distance to the saved
|
||||
// bounds and set it as the target display for restoration.
|
||||
int min_displacement = std::numeric_limits<int>::max();
|
||||
|
||||
for (const auto& candidate : screen->GetAllDisplays()) {
|
||||
gfx::Rect test_bounds = saved_bounds;
|
||||
test_bounds.AdjustToFit(candidate.work_area());
|
||||
int displacement = std::abs(test_bounds.x() - saved_bounds.x()) +
|
||||
std::abs(test_bounds.y() - saved_bounds.y());
|
||||
|
||||
if (displacement < min_displacement) {
|
||||
min_displacement = displacement;
|
||||
display = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip window state restoration if current display has invalid dimensions or
|
||||
// is fake. Restoring from invalid displays (0x0) or fake displays (ID 0xFF)
|
||||
// could cause incorrect window positioning when later moved to real displays.
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:ui/display/types/display_constants.h;l=28;drc=e4f1aef5f3ec30a28950d766612cc2c04c822c71
|
||||
if (hasInvalidDisplay(display)) {
|
||||
LOG(WARNING) << "Window state not restored - no physical display attached "
|
||||
"or current display has invalid bounds";
|
||||
return;
|
||||
}
|
||||
|
||||
gfx::Rect saved_work_area = gfx::Rect(*work_area_left, *work_area_top,
|
||||
*work_area_right - *work_area_left,
|
||||
*work_area_bottom - *work_area_top);
|
||||
|
||||
// Set this to true before RestoreBounds to prevent SaveWindowState from being
|
||||
// inadvertently triggered during the restoration process.
|
||||
is_being_restored_ = true;
|
||||
|
||||
if (restore_bounds_) {
|
||||
RestoreBounds(display, saved_work_area, saved_bounds);
|
||||
}
|
||||
|
||||
if (restore_display_mode_) {
|
||||
restore_display_mode_callback_ = base::BindOnce(
|
||||
[](NativeWindow* window, base::Value::Dict prefs) {
|
||||
if (auto kiosk = prefs.FindBool(electron::kKiosk); kiosk && *kiosk) {
|
||||
window->SetKiosk(true);
|
||||
} else if (auto fs = prefs.FindBool(electron::kFullscreen);
|
||||
fs && *fs) {
|
||||
window->SetFullScreen(true);
|
||||
} else if (auto max = prefs.FindBool(electron::kMaximized);
|
||||
max && *max) {
|
||||
window->Maximize();
|
||||
}
|
||||
},
|
||||
base::Unretained(this), window_preferences->Clone());
|
||||
}
|
||||
|
||||
is_being_restored_ = false;
|
||||
|
||||
NotifyWindowStateRestored();
|
||||
}
|
||||
|
||||
void NativeWindow::FlushPendingDisplayMode() {
|
||||
if (restore_display_mode_callback_) {
|
||||
std::move(restore_display_mode_callback_).Run();
|
||||
}
|
||||
}
|
||||
|
||||
// This function is similar to Chromium's window bounds adjustment logic
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/window_sizer/window_sizer.cc;l=350;drc=0ec56065ba588552f21633aa47280ba02c3cd160
|
||||
void NativeWindow::RestoreBounds(const display::Display& display,
|
||||
const gfx::Rect& saved_work_area,
|
||||
gfx::Rect& saved_bounds) {
|
||||
if (saved_bounds.width() == 0 || saved_bounds.height() == 0) {
|
||||
LOG(WARNING) << "Window bounds not restored - values are invalid";
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure that the window is at least kMinVisibleHeight * kMinVisibleWidth.
|
||||
saved_bounds.set_height(std::max(kMinVisibleHeight, saved_bounds.height()));
|
||||
saved_bounds.set_width(std::max(kMinVisibleWidth, saved_bounds.width()));
|
||||
|
||||
const gfx::Rect work_area = display.work_area();
|
||||
// Ensure that the title bar is not above the work area.
|
||||
if (saved_bounds.y() < work_area.y()) {
|
||||
saved_bounds.set_y(work_area.y());
|
||||
}
|
||||
|
||||
// Reposition and resize the bounds if the saved_work_area is different from
|
||||
// the current work area and the current work area doesn't completely contain
|
||||
// the bounds.
|
||||
if (!saved_work_area.IsEmpty() && saved_work_area != work_area &&
|
||||
!work_area.Contains(saved_bounds)) {
|
||||
saved_bounds.AdjustToFit(work_area);
|
||||
}
|
||||
|
||||
#if BUILDFLAG(IS_MAC)
|
||||
// On mac, we want to be aggressive about repositioning windows that are
|
||||
// partially offscreen. If the window is partially offscreen horizontally,
|
||||
// snap to the nearest edge of the work area. This call also adjusts the
|
||||
// height, width if needed to make the window fully visible.
|
||||
saved_bounds.AdjustToFit(work_area);
|
||||
#else
|
||||
// On non-Mac platforms, we are less aggressive about repositioning. Simply
|
||||
// ensure that at least kMinVisibleWidth * kMinVisibleHeight is visible
|
||||
|
||||
const int min_y = work_area.y() + kMinVisibleHeight - saved_bounds.height();
|
||||
const int min_x = work_area.x() + kMinVisibleWidth - saved_bounds.width();
|
||||
const int max_y = work_area.bottom() - kMinVisibleHeight;
|
||||
const int max_x = work_area.right() - kMinVisibleWidth;
|
||||
// Reposition and resize the bounds to make it fully visible inside the work
|
||||
// area. `min_x >= max_x` happens when work area and bounds are both small.
|
||||
if (min_x >= max_x || min_y >= max_y) {
|
||||
saved_bounds.AdjustToFit(work_area);
|
||||
} else {
|
||||
saved_bounds.set_y(std::clamp(saved_bounds.y(), min_y, max_y));
|
||||
saved_bounds.set_x(std::clamp(saved_bounds.x(), min_x, max_x));
|
||||
}
|
||||
#endif // BUILDFLAG(IS_MAC)
|
||||
|
||||
SetBounds(saved_bounds);
|
||||
}
|
||||
|
||||
// static
|
||||
bool NativeWindow::PlatformHasClientFrame() {
|
||||
#if defined(USE_OZONE)
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
#include "base/memory/weak_ptr.h"
|
||||
#include "base/observer_list.h"
|
||||
#include "base/strings/cstring_view.h"
|
||||
#include "base/supports_user_data.h"
|
||||
#include "base/timer/timer.h"
|
||||
#include "content/public/browser/desktop_media_id.h"
|
||||
#include "content/public/browser/web_contents_user_data.h"
|
||||
#include "extensions/browser/app_window/size_constraints.h"
|
||||
@@ -26,6 +28,7 @@
|
||||
|
||||
class SkRegion;
|
||||
class DraggableRegionProvider;
|
||||
class PrefService;
|
||||
|
||||
namespace input {
|
||||
struct NativeWebKeyboardEvent;
|
||||
@@ -164,9 +167,11 @@ class NativeWindow : public views::WidgetDelegate {
|
||||
void SetTitle(std::string_view title);
|
||||
[[nodiscard]] std::string GetTitle() const;
|
||||
|
||||
[[nodiscard]] std::string GetName() const;
|
||||
|
||||
// Ability to augment the window title for the screen readers.
|
||||
void SetAccessibleTitle(const std::string& title);
|
||||
std::string GetAccessibleTitle();
|
||||
[[nodiscard]] std::string GetAccessibleTitle() const;
|
||||
|
||||
virtual void FlashFrame(bool flash) = 0;
|
||||
virtual void SetSkipTaskbar(bool skip) = 0;
|
||||
@@ -339,6 +344,7 @@ class NativeWindow : public views::WidgetDelegate {
|
||||
void NotifyNewWindowForTab();
|
||||
void NotifyWindowSystemContextMenu(int x, int y, bool* prevent_default);
|
||||
void NotifyLayoutWindowControlsOverlay();
|
||||
void NotifyWindowStateRestored();
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
void NotifyWindowMessage(UINT message, WPARAM w_param, LPARAM l_param);
|
||||
@@ -430,6 +436,28 @@ class NativeWindow : public views::WidgetDelegate {
|
||||
|
||||
[[nodiscard]] auto base_window_id() const { return base_window_id_; }
|
||||
|
||||
// Saves current window state to the Local State JSON file in
|
||||
// app.getPath('userData') via PrefService.
|
||||
// This does NOT immediately write to disk - it updates the in-memory
|
||||
// preference store and queues an asynchronous write operation. The actual
|
||||
// disk write is batched and flushed later.
|
||||
void SaveWindowState();
|
||||
void DebouncedSaveWindowState();
|
||||
// Flushes save_window_state_timer_ that was queued by
|
||||
// DebouncedSaveWindowState. This does NOT flush the actual disk write.
|
||||
void FlushWindowState();
|
||||
|
||||
// Restores window state - bounds first and then display mode.
|
||||
void RestoreWindowState(const gin_helper::Dictionary& options);
|
||||
// Applies saved bounds to the window.
|
||||
void RestoreBounds(const display::Display& display,
|
||||
const gfx::Rect& saved_work_area,
|
||||
gfx::Rect& saved_bounds);
|
||||
// Flushes pending display mode restoration (fullscreen, maximized, kiosk)
|
||||
// that was deferred during initialization to respect show=false. This
|
||||
// consumes and clears the restore_display_mode_callback_.
|
||||
void FlushPendingDisplayMode();
|
||||
|
||||
protected:
|
||||
NativeWindow(int32_t base_window_id,
|
||||
const gin_helper::Dictionary& options,
|
||||
@@ -494,6 +522,10 @@ class NativeWindow : public views::WidgetDelegate {
|
||||
// ID of the api::BaseWindow that owns this NativeWindow.
|
||||
const int32_t base_window_id_;
|
||||
|
||||
// Identifier for the window provided by the application.
|
||||
// Used by Electron internally for features such as state persistence.
|
||||
std::string window_name_;
|
||||
|
||||
// The "titleBarStyle" option.
|
||||
const TitleBarStyle title_bar_style_;
|
||||
|
||||
@@ -552,6 +584,32 @@ class NativeWindow : public views::WidgetDelegate {
|
||||
|
||||
gfx::Rect overlay_rect_;
|
||||
|
||||
// Flag to prevent SaveWindowState calls during window restoration.
|
||||
bool is_being_restored_ = false;
|
||||
|
||||
// The boolean parsing of the "windowStatePersistence" option
|
||||
bool window_state_persistence_enabled_ = false;
|
||||
|
||||
// PrefService is used to persist window bounds and state.
|
||||
// Only populated when windowStatePersistence is enabled and window has a
|
||||
// valid name.
|
||||
raw_ptr<PrefService> prefs_ = nullptr;
|
||||
|
||||
// Whether to restore bounds.
|
||||
bool restore_bounds_ = false;
|
||||
// Whether to restore display mode.
|
||||
bool restore_display_mode_ = false;
|
||||
// Callback to restore display mode.
|
||||
base::OnceCallback<void()> restore_display_mode_callback_;
|
||||
|
||||
// Timer to debounce window state saving operations.
|
||||
base::OneShotTimer save_window_state_timer_;
|
||||
|
||||
// Minimum height of the visible part of a window.
|
||||
const int kMinVisibleHeight = 100;
|
||||
// Minimum width of the visible part of a window.
|
||||
const int kMinVisibleWidth = 100;
|
||||
|
||||
base::WeakPtrFactory<NativeWindow> weak_factory_{this};
|
||||
};
|
||||
|
||||
|
||||
@@ -468,6 +468,8 @@ void NativeWindowMac::Show() {
|
||||
return;
|
||||
}
|
||||
|
||||
FlushPendingDisplayMode();
|
||||
|
||||
set_wants_to_be_visible(true);
|
||||
|
||||
// Reattach the window to the parent to actually show it.
|
||||
|
||||
@@ -110,6 +110,8 @@ class NativeWindowObserver : public base::CheckedObserver {
|
||||
virtual void OnExecuteAppCommand(std::string_view command_name) {}
|
||||
|
||||
virtual void UpdateWindowControlsOverlay(const gfx::Rect& bounding_rect) {}
|
||||
|
||||
virtual void OnWindowStateRestored() {}
|
||||
};
|
||||
|
||||
} // namespace electron
|
||||
|
||||
@@ -571,6 +571,8 @@ void NativeWindowViews::Show() {
|
||||
if (is_modal() && NativeWindow::parent() && !widget()->IsVisible())
|
||||
static_cast<NativeWindowViews*>(parent())->IncrementChildModals();
|
||||
|
||||
FlushPendingDisplayMode();
|
||||
|
||||
widget()->native_widget_private()->Show(GetRestoredState(), gfx::Rect());
|
||||
|
||||
// explicitly focus the window
|
||||
|
||||
@@ -72,7 +72,8 @@ std::wstring NotificationPresenterWin::SaveIconToFilesystem(
|
||||
|
||||
std::string filename;
|
||||
if (origin.is_valid()) {
|
||||
filename = base::SHA1HashString(origin.spec()) + ".png";
|
||||
const auto hash = base::SHA1HashString(origin.spec());
|
||||
filename = base::HexEncode(hash) + ".png";
|
||||
} else {
|
||||
const int64_t now_usec = base::Time::Now().since_origin().InMicroseconds();
|
||||
filename = base::NumberToString(now_usec) + ".png";
|
||||
|
||||
@@ -59,6 +59,8 @@ gfx::Size GetDefaultPrinterDPI(const std::u16string& device_name) {
|
||||
GtkPrintSettings* print_settings = gtk_print_settings_new();
|
||||
int dpi = gtk_print_settings_get_resolution(print_settings);
|
||||
g_object_unref(print_settings);
|
||||
if (dpi <= 0)
|
||||
dpi = printing::kDefaultPdfDpi;
|
||||
return {dpi, dpi};
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -378,7 +378,11 @@ gfx::Image Clipboard::ReadImage(gin::Arguments* const args) {
|
||||
[](std::optional<gfx::Image>* image, base::RepeatingClosure cb,
|
||||
const std::vector<uint8_t>& result) {
|
||||
SkBitmap bitmap = gfx::PNGCodec::Decode(result);
|
||||
image->emplace(gfx::Image::CreateFrom1xBitmap(bitmap));
|
||||
if (bitmap.isNull()) {
|
||||
image->emplace();
|
||||
} else {
|
||||
image->emplace(gfx::Image::CreateFrom1xBitmap(bitmap));
|
||||
}
|
||||
std::move(cb).Run();
|
||||
},
|
||||
&image, std::move(callback)));
|
||||
|
||||
@@ -2,13 +2,17 @@
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "base/command_line.h"
|
||||
#include "base/dcheck_is_on.h"
|
||||
#include "base/logging.h"
|
||||
#include "base/no_destructor.h"
|
||||
#include "content/browser/network_service_instance_impl.h" // nogncheck
|
||||
#include "content/public/browser/network_service_instance.h"
|
||||
#include "content/public/common/content_switches.h"
|
||||
#include "shell/common/callback_util.h"
|
||||
#include "shell/common/gin_converters/callback_converter.h"
|
||||
#include "shell/common/gin_helper/dictionary.h"
|
||||
#include "shell/common/gin_helper/promise.h"
|
||||
#include "shell/common/node_includes.h"
|
||||
@@ -17,6 +21,93 @@
|
||||
#if DCHECK_IS_ON()
|
||||
namespace {
|
||||
|
||||
class CallbackTestingHelper final {
|
||||
public:
|
||||
void HoldRepeatingCallback(const base::RepeatingClosure& callback) {
|
||||
repeating_callback_ = callback;
|
||||
}
|
||||
|
||||
bool CopyHeldRepeatingCallback() {
|
||||
if (!repeating_callback_)
|
||||
return false;
|
||||
|
||||
repeating_callback_copy_ = *repeating_callback_;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool InvokeHeldRepeatingCallback(v8::Isolate* isolate) {
|
||||
if (!repeating_callback_)
|
||||
return false;
|
||||
|
||||
return InvokeRepeatingCallback(isolate, *repeating_callback_);
|
||||
}
|
||||
|
||||
bool InvokeCopiedRepeatingCallback(v8::Isolate* isolate) {
|
||||
if (!repeating_callback_copy_)
|
||||
return false;
|
||||
|
||||
return InvokeRepeatingCallback(isolate, *repeating_callback_copy_);
|
||||
}
|
||||
|
||||
void HoldOnceCallback(base::OnceClosure callback) {
|
||||
once_callback_ = std::move(callback);
|
||||
}
|
||||
|
||||
bool InvokeHeldOnceCallback(v8::Isolate* isolate) {
|
||||
if (!once_callback_)
|
||||
return false;
|
||||
|
||||
base::OnceClosure callback = std::move(*once_callback_);
|
||||
once_callback_.reset();
|
||||
return InvokeOnceCallback(isolate, std::move(callback));
|
||||
}
|
||||
|
||||
void ClearPrimaryHeldRepeatingCallback() { repeating_callback_.reset(); }
|
||||
|
||||
int GetHeldRepeatingCallbackCount() const {
|
||||
return (repeating_callback_ ? 1 : 0) + (repeating_callback_copy_ ? 1 : 0);
|
||||
}
|
||||
|
||||
void ClearAllHeldCallbacks() {
|
||||
repeating_callback_.reset();
|
||||
repeating_callback_copy_.reset();
|
||||
once_callback_.reset();
|
||||
}
|
||||
|
||||
private:
|
||||
bool InvokeRepeatingCallback(v8::Isolate* isolate,
|
||||
const base::RepeatingClosure& callback) {
|
||||
v8::TryCatch try_catch(isolate);
|
||||
callback.Run();
|
||||
if (try_catch.HasCaught()) {
|
||||
try_catch.Reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool InvokeOnceCallback(v8::Isolate* isolate, base::OnceClosure callback) {
|
||||
v8::TryCatch try_catch(isolate);
|
||||
std::move(callback).Run();
|
||||
if (try_catch.HasCaught()) {
|
||||
try_catch.Reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<base::RepeatingClosure> repeating_callback_;
|
||||
std::optional<base::RepeatingClosure> repeating_callback_copy_;
|
||||
std::optional<base::OnceClosure> once_callback_;
|
||||
};
|
||||
|
||||
CallbackTestingHelper& GetCallbackTestingHelper() {
|
||||
static base::NoDestructor<CallbackTestingHelper> helper;
|
||||
return *helper;
|
||||
}
|
||||
|
||||
void Log(int severity, std::string text) {
|
||||
switch (severity) {
|
||||
case logging::LOGGING_VERBOSE:
|
||||
@@ -57,6 +148,44 @@ v8::Local<v8::Promise> SimulateNetworkServiceCrash(v8::Isolate* isolate) {
|
||||
return handle;
|
||||
}
|
||||
|
||||
void HoldRepeatingCallbackForTesting(const base::RepeatingClosure& callback) {
|
||||
GetCallbackTestingHelper().HoldRepeatingCallback(callback);
|
||||
}
|
||||
|
||||
bool CopyHeldRepeatingCallbackForTesting() {
|
||||
return GetCallbackTestingHelper().CopyHeldRepeatingCallback();
|
||||
}
|
||||
|
||||
bool InvokeHeldRepeatingCallbackForTesting(gin::Arguments* args) {
|
||||
return GetCallbackTestingHelper().InvokeHeldRepeatingCallback(
|
||||
args->isolate());
|
||||
}
|
||||
|
||||
bool InvokeCopiedRepeatingCallbackForTesting(gin::Arguments* args) {
|
||||
return GetCallbackTestingHelper().InvokeCopiedRepeatingCallback(
|
||||
args->isolate());
|
||||
}
|
||||
|
||||
void HoldOnceCallbackForTesting(base::OnceClosure callback) {
|
||||
GetCallbackTestingHelper().HoldOnceCallback(std::move(callback));
|
||||
}
|
||||
|
||||
bool InvokeHeldOnceCallbackForTesting(gin::Arguments* args) {
|
||||
return GetCallbackTestingHelper().InvokeHeldOnceCallback(args->isolate());
|
||||
}
|
||||
|
||||
void ClearPrimaryHeldRepeatingCallbackForTesting() {
|
||||
GetCallbackTestingHelper().ClearPrimaryHeldRepeatingCallback();
|
||||
}
|
||||
|
||||
int GetHeldRepeatingCallbackCountForTesting() {
|
||||
return GetCallbackTestingHelper().GetHeldRepeatingCallbackCount();
|
||||
}
|
||||
|
||||
void ClearHeldCallbacksForTesting() {
|
||||
GetCallbackTestingHelper().ClearAllHeldCallbacks();
|
||||
}
|
||||
|
||||
void Initialize(v8::Local<v8::Object> exports,
|
||||
v8::Local<v8::Value> unused,
|
||||
v8::Local<v8::Context> context,
|
||||
@@ -66,6 +195,22 @@ void Initialize(v8::Local<v8::Object> exports,
|
||||
dict.SetMethod("log", &Log);
|
||||
dict.SetMethod("getLoggingDestination", &GetLoggingDestination);
|
||||
dict.SetMethod("simulateNetworkServiceCrash", &SimulateNetworkServiceCrash);
|
||||
dict.SetMethod("holdRepeatingCallbackForTesting",
|
||||
&HoldRepeatingCallbackForTesting);
|
||||
dict.SetMethod("copyHeldRepeatingCallbackForTesting",
|
||||
&CopyHeldRepeatingCallbackForTesting);
|
||||
dict.SetMethod("invokeHeldRepeatingCallbackForTesting",
|
||||
&InvokeHeldRepeatingCallbackForTesting);
|
||||
dict.SetMethod("invokeCopiedRepeatingCallbackForTesting",
|
||||
&InvokeCopiedRepeatingCallbackForTesting);
|
||||
dict.SetMethod("clearPrimaryHeldRepeatingCallbackForTesting",
|
||||
&ClearPrimaryHeldRepeatingCallbackForTesting);
|
||||
dict.SetMethod("getHeldRepeatingCallbackCountForTesting",
|
||||
&GetHeldRepeatingCallbackCountForTesting);
|
||||
dict.SetMethod("holdOnceCallbackForTesting", &HoldOnceCallbackForTesting);
|
||||
dict.SetMethod("invokeHeldOnceCallbackForTesting",
|
||||
&InvokeHeldOnceCallbackForTesting);
|
||||
dict.SetMethod("clearHeldCallbacksForTesting", &ClearHeldCallbacksForTesting);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -21,6 +21,23 @@ inline constexpr std::string_view kDeviceVendorIdKey = "vendorId";
|
||||
inline constexpr std::string_view kDeviceProductIdKey = "productId";
|
||||
inline constexpr std::string_view kDeviceSerialNumberKey = "serialNumber";
|
||||
|
||||
// Window state preference keys
|
||||
inline constexpr std::string_view kLeft = "left";
|
||||
inline constexpr std::string_view kTop = "top";
|
||||
inline constexpr std::string_view kRight = "right";
|
||||
inline constexpr std::string_view kBottom = "bottom";
|
||||
|
||||
inline constexpr std::string_view kMaximized = "maximized";
|
||||
inline constexpr std::string_view kFullscreen = "fullscreen";
|
||||
inline constexpr std::string_view kKiosk = "kiosk";
|
||||
|
||||
inline constexpr std::string_view kWorkAreaLeft = "workAreaLeft";
|
||||
inline constexpr std::string_view kWorkAreaTop = "workAreaTop";
|
||||
inline constexpr std::string_view kWorkAreaRight = "workAreaRight";
|
||||
inline constexpr std::string_view kWorkAreaBottom = "workAreaBottom";
|
||||
|
||||
inline constexpr std::string_view kWindowStates = "windowStates";
|
||||
|
||||
inline constexpr base::cstring_view kRunAsNode = "ELECTRON_RUN_AS_NODE";
|
||||
|
||||
// Per-profile UUID to distinguish global shortcut sessions for
|
||||
|
||||
@@ -155,9 +155,12 @@ v8::Local<v8::Value> Converter<electron::OffscreenSharedTextureValue>::ToV8(
|
||||
root.Set("textureInfo", ConvertToV8(isolate, dict));
|
||||
auto root_local = ConvertToV8(isolate, root);
|
||||
|
||||
// Create a persistent reference of the object, so that we can check the
|
||||
// monitor again when GC collects this object.
|
||||
auto* tex_persistent = monitor->CreatePersistent(isolate, root_local);
|
||||
// Create a weak persistent that tracks the release function rather than the
|
||||
// texture object. The release function holds a raw pointer to |monitor| via
|
||||
// its v8::External data, so |monitor| must outlive it. Since the texture
|
||||
// keeps |release| alive via its property, this also covers the case where
|
||||
// the texture itself is leaked without calling release().
|
||||
auto* tex_persistent = monitor->CreatePersistent(isolate, releaser);
|
||||
tex_persistent->SetWeak(
|
||||
monitor,
|
||||
[](const v8::WeakCallbackInfo<OffscreenReleaseHolderMonitor>& data) {
|
||||
|
||||
@@ -4,12 +4,32 @@
|
||||
|
||||
#include "shell/common/gin_helper/callback.h"
|
||||
|
||||
#include "content/public/browser/browser_thread.h"
|
||||
#include "gin/dictionary.h"
|
||||
#include "shell/common/process_util.h"
|
||||
#include "gin/persistent.h"
|
||||
#include "v8/include/cppgc/allocation.h"
|
||||
#include "v8/include/v8-cppgc.h"
|
||||
#include "v8/include/v8-traced-handle.h"
|
||||
|
||||
namespace gin_helper {
|
||||
|
||||
class SafeV8FunctionHandle final
|
||||
: public cppgc::GarbageCollected<SafeV8FunctionHandle> {
|
||||
public:
|
||||
SafeV8FunctionHandle(v8::Isolate* isolate, v8::Local<v8::Value> value)
|
||||
: v8_function_(isolate, value.As<v8::Function>()) {}
|
||||
|
||||
void Trace(cppgc::Visitor* visitor) const { visitor->Trace(v8_function_); }
|
||||
|
||||
[[nodiscard]] bool IsAlive() const { return !v8_function_.IsEmpty(); }
|
||||
|
||||
v8::Local<v8::Function> NewHandle(v8::Isolate* isolate) const {
|
||||
return v8_function_.Get(isolate);
|
||||
}
|
||||
|
||||
private:
|
||||
v8::TracedReference<v8::Function> v8_function_;
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
struct TranslatorHolder {
|
||||
@@ -71,46 +91,19 @@ void CallTranslator(v8::Local<v8::External> external,
|
||||
|
||||
} // namespace
|
||||
|
||||
// Destroy the class on UI thread when possible.
|
||||
struct DeleteOnUIThread {
|
||||
template <typename T>
|
||||
static void Destruct(const T* x) {
|
||||
if (electron::IsBrowserProcess() &&
|
||||
!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)) {
|
||||
content::GetUIThreadTaskRunner({})->DeleteSoon(FROM_HERE, x);
|
||||
} else {
|
||||
delete x;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Like v8::Global, but ref-counted.
|
||||
template <typename T>
|
||||
class RefCountedGlobal
|
||||
: public base::RefCountedThreadSafe<RefCountedGlobal<T>, DeleteOnUIThread> {
|
||||
public:
|
||||
RefCountedGlobal(v8::Isolate* isolate, v8::Local<v8::Value> value)
|
||||
: handle_(isolate, value.As<T>()) {}
|
||||
|
||||
[[nodiscard]] bool IsAlive() const { return !handle_.IsEmpty(); }
|
||||
|
||||
v8::Local<T> NewHandle(v8::Isolate* isolate) const {
|
||||
return v8::Local<T>::New(isolate, handle_);
|
||||
}
|
||||
|
||||
private:
|
||||
v8::Global<T> handle_;
|
||||
};
|
||||
|
||||
SafeV8Function::SafeV8Function(v8::Isolate* isolate, v8::Local<v8::Value> value)
|
||||
: v8_function_(new RefCountedGlobal<v8::Function>(isolate, value)) {}
|
||||
: v8_function_(
|
||||
gin::WrapPersistent(cppgc::MakeGarbageCollected<SafeV8FunctionHandle>(
|
||||
isolate->GetCppHeap()->GetAllocationHandle(),
|
||||
isolate,
|
||||
value))) {}
|
||||
|
||||
SafeV8Function::SafeV8Function(const SafeV8Function& other) = default;
|
||||
|
||||
SafeV8Function::~SafeV8Function() = default;
|
||||
|
||||
bool SafeV8Function::IsAlive() const {
|
||||
return v8_function_.get() && v8_function_->IsAlive();
|
||||
return v8_function_ && v8_function_->IsAlive();
|
||||
}
|
||||
|
||||
v8::Local<v8::Function> SafeV8Function::NewHandle(v8::Isolate* isolate) const {
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
#include "shell/common/gin_converters/std_converter.h"
|
||||
#include "shell/common/gin_helper/function_template.h"
|
||||
#include "shell/common/gin_helper/locker.h"
|
||||
#include "v8/include/cppgc/persistent.h"
|
||||
#include "v8/include/v8-function.h"
|
||||
#include "v8/include/v8-microtask-queue.h"
|
||||
// Implements safe conversions between JS functions and base::RepeatingCallback.
|
||||
|
||||
namespace gin_helper {
|
||||
|
||||
template <typename T>
|
||||
class RefCountedGlobal;
|
||||
class SafeV8FunctionHandle;
|
||||
|
||||
// Manages the V8 function with RAII.
|
||||
class SafeV8Function {
|
||||
@@ -32,7 +32,7 @@ class SafeV8Function {
|
||||
v8::Local<v8::Function> NewHandle(v8::Isolate* isolate) const;
|
||||
|
||||
private:
|
||||
scoped_refptr<RefCountedGlobal<v8::Function>> v8_function_;
|
||||
cppgc::Persistent<SafeV8FunctionHandle> v8_function_;
|
||||
};
|
||||
|
||||
// Helper to invoke a V8 function with C++ parameters.
|
||||
|
||||
@@ -107,6 +107,19 @@ inline constexpr std::string_view kFocusable = "focusable";
|
||||
// The WebPreferences.
|
||||
inline constexpr std::string_view kWebPreferences = "webPreferences";
|
||||
|
||||
// Window state persistence for BaseWindow
|
||||
inline constexpr std::string_view kWindowStatePersistence =
|
||||
"windowStatePersistence";
|
||||
|
||||
// Identifier for the window provided by the application
|
||||
inline constexpr std::string_view kName = "name";
|
||||
|
||||
// Whether to save the window bounds
|
||||
inline constexpr std::string_view kBounds = "bounds";
|
||||
|
||||
// Whether to save the window display mode
|
||||
inline constexpr std::string_view kDisplayMode = "displayMode";
|
||||
|
||||
// Add a vibrancy effect to the browser window
|
||||
inline constexpr std::string_view kVibrancyType = "vibrancy";
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -81,8 +81,8 @@ describe('safeStorage module', () => {
|
||||
});
|
||||
|
||||
describe('SafeStorage.isAsyncEncryptionAvailable()', () => {
|
||||
it('should return true when async encryption is available', () => {
|
||||
expect(safeStorage.isAsyncEncryptionAvailable()).to.equal(true);
|
||||
it('should resolve true when async encryption is available', async () => {
|
||||
expect(await safeStorage.isAsyncEncryptionAvailable()).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -993,6 +993,8 @@ describe('chromium features', () => {
|
||||
let w: BrowserWindow | null = null;
|
||||
|
||||
afterEach(() => {
|
||||
ipcMain.removeAllListeners('did-create-file-handle');
|
||||
ipcMain.removeAllListeners('did-create-directory-handle');
|
||||
session.defaultSession.setPermissionRequestHandler(null);
|
||||
closeAllWindows();
|
||||
});
|
||||
@@ -1110,6 +1112,7 @@ describe('chromium features', () => {
|
||||
w.webContents.once('did-finish-load', () => {
|
||||
// @ts-expect-error Undocumented testing method.
|
||||
clipboard._writeFilesForTesting([testFile]);
|
||||
w.webContents.focus();
|
||||
w.webContents.paste();
|
||||
});
|
||||
});
|
||||
@@ -1161,6 +1164,7 @@ describe('chromium features', () => {
|
||||
w.webContents.once('did-finish-load', () => {
|
||||
// @ts-expect-error Undocumented testing method.
|
||||
clipboard._writeFilesForTesting([testFile]);
|
||||
w.webContents.focus();
|
||||
w.webContents.paste();
|
||||
});
|
||||
});
|
||||
@@ -1212,6 +1216,7 @@ describe('chromium features', () => {
|
||||
w.webContents.once('did-finish-load', () => {
|
||||
// @ts-expect-error Undocumented testing method.
|
||||
clipboard._writeFilesForTesting([testFile]);
|
||||
w.webContents.focus();
|
||||
w.webContents.paste();
|
||||
});
|
||||
});
|
||||
@@ -1258,6 +1263,7 @@ describe('chromium features', () => {
|
||||
w.webContents.once('did-finish-load', () => {
|
||||
// @ts-expect-error Undocumented testing method.
|
||||
clipboard._writeFilesForTesting([testDir]);
|
||||
w.webContents.focus();
|
||||
w.webContents.paste();
|
||||
});
|
||||
});
|
||||
@@ -1305,6 +1311,7 @@ describe('chromium features', () => {
|
||||
w.webContents.once('did-finish-load', () => {
|
||||
// @ts-expect-error Undocumented testing method.
|
||||
clipboard._writeFilesForTesting([testDir]);
|
||||
w.webContents.focus();
|
||||
w.webContents.paste();
|
||||
});
|
||||
});
|
||||
@@ -1362,6 +1369,7 @@ describe('chromium features', () => {
|
||||
w.webContents.on('did-finish-load', () => {
|
||||
// @ts-expect-error Undocumented testing method.
|
||||
clipboard._writeFilesForTesting([testFile]);
|
||||
w.webContents.focus();
|
||||
w.webContents.paste();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { expect } from 'chai';
|
||||
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { startRemoteControlApp } from './lib/spec-helpers';
|
||||
import { ifdescribe, isTestingBindingAvailable, startRemoteControlApp } from './lib/spec-helpers';
|
||||
|
||||
describe('cpp heap', () => {
|
||||
describe('app module', () => {
|
||||
@@ -77,6 +77,191 @@ describe('cpp heap', () => {
|
||||
});
|
||||
});
|
||||
|
||||
ifdescribe(isTestingBindingAvailable())('SafeV8Function callback conversion', () => {
|
||||
const gcTestArgv = ['--js-flags=--expose-gc'];
|
||||
|
||||
it('retains repeating callback while held, allows multiple invocations, then releases', async () => {
|
||||
const { remotely } = await startRemoteControlApp(gcTestArgv);
|
||||
const result = await remotely(async () => {
|
||||
const testingBinding = (process as any)._linkedBinding('electron_common_testing');
|
||||
const v8Util = (process as any)._linkedBinding('electron_common_v8_util');
|
||||
|
||||
const waitForGC = async (fn: () => boolean) => {
|
||||
for (let i = 0; i < 30; ++i) {
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
v8Util.requestGarbageCollectionForTesting();
|
||||
if (fn()) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
let callCount = 0;
|
||||
let repeating: any = () => { callCount++; };
|
||||
const repeatingWeakRef = new WeakRef(repeating);
|
||||
testingBinding.holdRepeatingCallbackForTesting(repeating);
|
||||
repeating = null;
|
||||
|
||||
const invoked0 = testingBinding.invokeHeldRepeatingCallbackForTesting();
|
||||
const invoked1 = testingBinding.invokeHeldRepeatingCallbackForTesting();
|
||||
const invoked2 = testingBinding.invokeHeldRepeatingCallbackForTesting();
|
||||
|
||||
testingBinding.clearHeldCallbacksForTesting();
|
||||
const releasedAfterClear = await waitForGC(() => repeatingWeakRef.deref() === undefined);
|
||||
|
||||
return { invoked0, invoked1, invoked2, callCount, releasedAfterClear };
|
||||
});
|
||||
|
||||
expect(result.invoked0).to.equal(true, 'first invocation should succeed');
|
||||
expect(result.invoked1).to.equal(true, 'second invocation should succeed');
|
||||
expect(result.invoked2).to.equal(true, 'third invocation should succeed');
|
||||
expect(result.callCount).to.equal(3, 'callback should have been called 3 times');
|
||||
expect(result.releasedAfterClear).to.equal(true, 'callback should be released after clear');
|
||||
});
|
||||
|
||||
it('consumes once callback on first invoke and releases it', async () => {
|
||||
const { remotely } = await startRemoteControlApp(gcTestArgv);
|
||||
const result = await remotely(async () => {
|
||||
const testingBinding = (process as any)._linkedBinding('electron_common_testing');
|
||||
const v8Util = (process as any)._linkedBinding('electron_common_v8_util');
|
||||
|
||||
const waitForGC = async (fn: () => boolean) => {
|
||||
for (let i = 0; i < 30; ++i) {
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
v8Util.requestGarbageCollectionForTesting();
|
||||
if (fn()) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
let callCount = 0;
|
||||
let once: any = () => { callCount++; };
|
||||
const onceWeakRef = new WeakRef(once);
|
||||
testingBinding.holdOnceCallbackForTesting(once);
|
||||
once = null;
|
||||
|
||||
const first = testingBinding.invokeHeldOnceCallbackForTesting();
|
||||
const second = testingBinding.invokeHeldOnceCallbackForTesting();
|
||||
|
||||
testingBinding.clearHeldCallbacksForTesting();
|
||||
const released = await waitForGC(() => onceWeakRef.deref() === undefined);
|
||||
|
||||
return { first, second, callCount, released };
|
||||
});
|
||||
|
||||
expect(result.first).to.equal(true, 'first invoke should succeed');
|
||||
expect(result.second).to.equal(false, 'second invoke should fail (consumed)');
|
||||
expect(result.callCount).to.equal(1, 'callback should have been called once');
|
||||
expect(result.released).to.equal(true, 'callback should be released after consume + clear');
|
||||
});
|
||||
|
||||
it('releases replaced repeating callback while keeping latest callback alive', async () => {
|
||||
const { remotely } = await startRemoteControlApp(gcTestArgv);
|
||||
const result = await remotely(async () => {
|
||||
const testingBinding = (process as any)._linkedBinding('electron_common_testing');
|
||||
const v8Util = (process as any)._linkedBinding('electron_common_v8_util');
|
||||
|
||||
const waitForGC = async (fn: () => boolean) => {
|
||||
for (let i = 0; i < 30; ++i) {
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
v8Util.requestGarbageCollectionForTesting();
|
||||
if (fn()) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
let callbackA: any = () => {};
|
||||
const weakA = new WeakRef(callbackA);
|
||||
testingBinding.holdRepeatingCallbackForTesting(callbackA);
|
||||
callbackA = null;
|
||||
|
||||
let callbackB: any = () => {};
|
||||
const weakB = new WeakRef(callbackB);
|
||||
testingBinding.holdRepeatingCallbackForTesting(callbackB);
|
||||
callbackB = null;
|
||||
|
||||
const releasedA = await waitForGC(() => weakA.deref() === undefined);
|
||||
|
||||
testingBinding.clearHeldCallbacksForTesting();
|
||||
const releasedB = await waitForGC(() => weakB.deref() === undefined);
|
||||
|
||||
return { releasedA, releasedB };
|
||||
});
|
||||
|
||||
expect(result.releasedA).to.equal(true, 'replaced callback A should be released');
|
||||
expect(result.releasedB).to.equal(true, 'callback B should be released after clear');
|
||||
});
|
||||
|
||||
it('keeps callback alive while copied holder exists and releases after all copies clear', async () => {
|
||||
const { remotely } = await startRemoteControlApp(gcTestArgv);
|
||||
const result = await remotely(async () => {
|
||||
const testingBinding = (process as any)._linkedBinding('electron_common_testing');
|
||||
const v8Util = (process as any)._linkedBinding('electron_common_v8_util');
|
||||
|
||||
const waitForGC = async (fn: () => boolean) => {
|
||||
for (let i = 0; i < 30; ++i) {
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
v8Util.requestGarbageCollectionForTesting();
|
||||
if (fn()) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
let repeating: any = () => {};
|
||||
const weakRef = new WeakRef(repeating);
|
||||
testingBinding.holdRepeatingCallbackForTesting(repeating);
|
||||
repeating = null;
|
||||
|
||||
const copied = testingBinding.copyHeldRepeatingCallbackForTesting();
|
||||
const countAfterCopy = testingBinding.getHeldRepeatingCallbackCountForTesting();
|
||||
testingBinding.clearPrimaryHeldRepeatingCallbackForTesting();
|
||||
|
||||
const invokedViaCopy = testingBinding.invokeCopiedRepeatingCallbackForTesting();
|
||||
|
||||
testingBinding.clearHeldCallbacksForTesting();
|
||||
const releasedAfterClear = await waitForGC(() => weakRef.deref() === undefined);
|
||||
|
||||
return { copied, countAfterCopy, invokedViaCopy, releasedAfterClear };
|
||||
});
|
||||
|
||||
expect(result.copied).to.equal(true, 'copy should succeed');
|
||||
expect(result.countAfterCopy).to.equal(2, 'should have 2 holders after copy');
|
||||
expect(result.invokedViaCopy).to.equal(true, 'invoke via copy should succeed');
|
||||
expect(result.releasedAfterClear).to.equal(true, 'callback should be released after all copies clear');
|
||||
});
|
||||
|
||||
it('does not leak repeating callback when callback throws during invocation', async () => {
|
||||
const { remotely } = await startRemoteControlApp(gcTestArgv);
|
||||
const result = await remotely(async () => {
|
||||
const testingBinding = (process as any)._linkedBinding('electron_common_testing');
|
||||
const v8Util = (process as any)._linkedBinding('electron_common_v8_util');
|
||||
|
||||
const waitForGC = async (fn: () => boolean) => {
|
||||
for (let i = 0; i < 30; ++i) {
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
v8Util.requestGarbageCollectionForTesting();
|
||||
if (fn()) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
let throwing: any = () => { throw new Error('expected test throw'); };
|
||||
const weakRef = new WeakRef(throwing);
|
||||
testingBinding.holdRepeatingCallbackForTesting(throwing);
|
||||
throwing = null;
|
||||
|
||||
const invokeResult = testingBinding.invokeHeldRepeatingCallbackForTesting();
|
||||
|
||||
testingBinding.clearHeldCallbacksForTesting();
|
||||
const releasedAfterClear = await waitForGC(() => weakRef.deref() === undefined);
|
||||
|
||||
return { invokeResult, releasedAfterClear };
|
||||
});
|
||||
|
||||
expect(result.invokeResult).to.equal(false, 'invoke should fail (callback throws)');
|
||||
expect(result.releasedAfterClear).to.equal(true, 'throwing callback should be released after clear');
|
||||
});
|
||||
});
|
||||
|
||||
describe('internal event', () => {
|
||||
it('should record as node in heap snapshot', async () => {
|
||||
const { remotely } = await startRemoteControlApp(['--expose-internals']);
|
||||
|
||||
27
spec/fixtures/api/window-state-save/close-save/index.js
vendored
Normal file
27
spec/fixtures/api/window-state-save/close-save/index.js
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
const { app, BrowserWindow } = require('electron');
|
||||
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
|
||||
const sharedUserData = path.join(os.tmpdir(), 'electron-window-state-test');
|
||||
app.setPath('userData', sharedUserData);
|
||||
|
||||
app.whenReady().then(() => {
|
||||
const w = new BrowserWindow({
|
||||
width: 400,
|
||||
height: 300,
|
||||
name: 'test-close-save',
|
||||
windowStatePersistence: true,
|
||||
show: false
|
||||
});
|
||||
|
||||
w.on('close', () => {
|
||||
app.quit();
|
||||
});
|
||||
|
||||
w.close();
|
||||
// Timeout of 10s to ensure app exits
|
||||
setTimeout(() => {
|
||||
app.quit();
|
||||
}, 10000);
|
||||
});
|
||||
28
spec/fixtures/api/window-state-save/fullscreen-save/index.js
vendored
Normal file
28
spec/fixtures/api/window-state-save/fullscreen-save/index.js
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
const { app, BrowserWindow } = require('electron');
|
||||
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
|
||||
const sharedUserData = path.join(os.tmpdir(), 'electron-window-state-test');
|
||||
app.setPath('userData', sharedUserData);
|
||||
|
||||
app.whenReady().then(() => {
|
||||
const w = new BrowserWindow({
|
||||
width: 400,
|
||||
height: 300,
|
||||
name: 'test-fullscreen-save',
|
||||
windowStatePersistence: true
|
||||
});
|
||||
|
||||
w.on('enter-full-screen', () => {
|
||||
setTimeout(() => {
|
||||
app.quit();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
w.setFullScreen(true);
|
||||
// Timeout of 10s to ensure app exits
|
||||
setTimeout(() => {
|
||||
app.quit();
|
||||
}, 10000);
|
||||
});
|
||||
28
spec/fixtures/api/window-state-save/kiosk-save/index.js
vendored
Normal file
28
spec/fixtures/api/window-state-save/kiosk-save/index.js
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
const { app, BrowserWindow } = require('electron');
|
||||
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
|
||||
const sharedUserData = path.join(os.tmpdir(), 'electron-window-state-test');
|
||||
app.setPath('userData', sharedUserData);
|
||||
|
||||
app.whenReady().then(() => {
|
||||
const w = new BrowserWindow({
|
||||
width: 400,
|
||||
height: 300,
|
||||
name: 'test-kiosk-save',
|
||||
windowStatePersistence: true
|
||||
});
|
||||
|
||||
w.on('enter-full-screen', () => {
|
||||
setTimeout(() => {
|
||||
app.quit();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
w.setKiosk(true);
|
||||
// Timeout of 10s to ensure app exits
|
||||
setTimeout(() => {
|
||||
app.quit();
|
||||
}, 10000);
|
||||
});
|
||||
28
spec/fixtures/api/window-state-save/maximize-save/index.js
vendored
Normal file
28
spec/fixtures/api/window-state-save/maximize-save/index.js
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
const { app, BrowserWindow } = require('electron');
|
||||
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
|
||||
const sharedUserData = path.join(os.tmpdir(), 'electron-window-state-test');
|
||||
app.setPath('userData', sharedUserData);
|
||||
|
||||
app.whenReady().then(() => {
|
||||
const w = new BrowserWindow({
|
||||
width: 400,
|
||||
height: 300,
|
||||
name: 'test-maximize-save',
|
||||
windowStatePersistence: true
|
||||
});
|
||||
|
||||
w.on('maximize', () => {
|
||||
setTimeout(() => {
|
||||
app.quit();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
w.maximize();
|
||||
// Timeout of 10s to ensure app exits
|
||||
setTimeout(() => {
|
||||
app.quit();
|
||||
}, 10000);
|
||||
});
|
||||
28
spec/fixtures/api/window-state-save/minimize-save/index.js
vendored
Normal file
28
spec/fixtures/api/window-state-save/minimize-save/index.js
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
const { app, BrowserWindow } = require('electron');
|
||||
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
|
||||
const sharedUserData = path.join(os.tmpdir(), 'electron-window-state-test');
|
||||
app.setPath('userData', sharedUserData);
|
||||
|
||||
app.whenReady().then(() => {
|
||||
const w = new BrowserWindow({
|
||||
width: 400,
|
||||
height: 300,
|
||||
name: 'test-minimize-save',
|
||||
windowStatePersistence: true
|
||||
});
|
||||
|
||||
w.on('minimize', () => {
|
||||
setTimeout(() => {
|
||||
app.quit();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
w.minimize();
|
||||
// Timeout of 10s to ensure app exits
|
||||
setTimeout(() => {
|
||||
app.quit();
|
||||
}, 10000);
|
||||
});
|
||||
23
spec/fixtures/api/window-state-save/move-save/index.js
vendored
Normal file
23
spec/fixtures/api/window-state-save/move-save/index.js
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
const { app, BrowserWindow } = require('electron');
|
||||
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
|
||||
const sharedUserData = path.join(os.tmpdir(), 'electron-window-state-test');
|
||||
app.setPath('userData', sharedUserData);
|
||||
|
||||
app.whenReady().then(() => {
|
||||
const w = new BrowserWindow({
|
||||
width: 400,
|
||||
height: 300,
|
||||
name: 'test-move-save',
|
||||
windowStatePersistence: true,
|
||||
show: false
|
||||
});
|
||||
|
||||
w.setPosition(100, 150);
|
||||
|
||||
setTimeout(() => {
|
||||
app.quit();
|
||||
}, 1000);
|
||||
});
|
||||
23
spec/fixtures/api/window-state-save/resize-save/index.js
vendored
Normal file
23
spec/fixtures/api/window-state-save/resize-save/index.js
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
const { app, BrowserWindow } = require('electron');
|
||||
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
|
||||
const sharedUserData = path.join(os.tmpdir(), 'electron-window-state-test');
|
||||
app.setPath('userData', sharedUserData);
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
const w = new BrowserWindow({
|
||||
width: 400,
|
||||
height: 300,
|
||||
name: 'test-resize-save',
|
||||
windowStatePersistence: true,
|
||||
show: false
|
||||
});
|
||||
|
||||
w.setSize(500, 400);
|
||||
|
||||
setTimeout(() => {
|
||||
app.quit();
|
||||
}, 1000);
|
||||
});
|
||||
23
spec/fixtures/api/window-state-save/schema-check/index.js
vendored
Normal file
23
spec/fixtures/api/window-state-save/schema-check/index.js
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
const { app, BrowserWindow } = require('electron');
|
||||
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
|
||||
const sharedUserData = path.join(os.tmpdir(), 'electron-window-state-test');
|
||||
app.setPath('userData', sharedUserData);
|
||||
|
||||
app.whenReady().then(() => {
|
||||
const w = new BrowserWindow({
|
||||
width: 400,
|
||||
height: 300,
|
||||
name: 'test-window-state-schema',
|
||||
windowStatePersistence: true,
|
||||
show: true
|
||||
});
|
||||
|
||||
w.close();
|
||||
|
||||
setTimeout(() => {
|
||||
app.quit();
|
||||
}, 1000);
|
||||
});
|
||||
41
spec/fixtures/api/window-state-save/work-area-primary/index.js
vendored
Normal file
41
spec/fixtures/api/window-state-save/work-area-primary/index.js
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
const { app, BrowserWindow, screen } = require('electron');
|
||||
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
|
||||
const sharedUserData = path.join(os.tmpdir(), 'electron-window-state-test');
|
||||
app.setPath('userData', sharedUserData);
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
const primaryDisplay = screen.getPrimaryDisplay();
|
||||
const workArea = primaryDisplay.workArea;
|
||||
|
||||
const maxWidth = Math.max(200, Math.floor(workArea.width * 0.8));
|
||||
const maxHeight = Math.max(150, Math.floor(workArea.height * 0.8));
|
||||
const windowWidth = Math.min(400, maxWidth);
|
||||
const windowHeight = Math.min(300, maxHeight);
|
||||
|
||||
const w = new BrowserWindow({
|
||||
width: windowWidth,
|
||||
height: windowHeight,
|
||||
name: 'test-work-area-primary',
|
||||
windowStatePersistence: true
|
||||
});
|
||||
|
||||
// Center the window on the primary display to prevent overflow
|
||||
const centerX = workArea.x + Math.floor((workArea.width - windowWidth) / 2);
|
||||
const centerY = workArea.y + Math.floor((workArea.height - windowHeight) / 2);
|
||||
|
||||
w.setPosition(centerX, centerY);
|
||||
|
||||
w.on('close', () => {
|
||||
app.quit();
|
||||
});
|
||||
|
||||
w.close();
|
||||
|
||||
// Timeout of 10s to ensure app exits
|
||||
setTimeout(() => {
|
||||
app.quit();
|
||||
}, 10000);
|
||||
});
|
||||
90
spec/fixtures/native-addon/virtual-display/binding.gyp
vendored
Normal file
90
spec/fixtures/native-addon/virtual-display/binding.gyp
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"targets": [{
|
||||
"target_name": "virtual_display",
|
||||
"conditions": [
|
||||
['OS=="mac"', {
|
||||
"sources": [
|
||||
"src/addon.mm",
|
||||
"src/VirtualDisplayBridge.m"
|
||||
],
|
||||
"include_dirs": [
|
||||
"<!@(node -p \"require('node-addon-api').include\")",
|
||||
"include",
|
||||
"build_swift"
|
||||
],
|
||||
"dependencies": [
|
||||
"<!(node -p \"require('node-addon-api').gyp\")"
|
||||
],
|
||||
"libraries": [
|
||||
"<(PRODUCT_DIR)/libVirtualDisplay.dylib"
|
||||
],
|
||||
"defines": [
|
||||
"NODE_ADDON_API_CPP_EXCEPTIONS"
|
||||
],
|
||||
"cflags!": [ "-fno-exceptions" ],
|
||||
"cflags_cc!": [ "-fno-exceptions" ],
|
||||
"xcode_settings": {
|
||||
"GCC_ENABLE_CPP_EXCEPTIONS": "YES",
|
||||
"CLANG_ENABLE_OBJC_ARC": "YES",
|
||||
"CLANG_CXX_LIBRARY": "libc++",
|
||||
"SWIFT_OBJC_BRIDGING_HEADER": "include/VirtualDisplayBridge.h",
|
||||
"SWIFT_VERSION": "5.0",
|
||||
"SWIFT_OBJC_INTERFACE_HEADER_NAME": "virtual_display-Swift.h",
|
||||
"MACOSX_DEPLOYMENT_TARGET": "11.0",
|
||||
"OTHER_CFLAGS": [
|
||||
"-ObjC++",
|
||||
"-fobjc-arc"
|
||||
],
|
||||
"OTHER_LDFLAGS": [
|
||||
"-lswiftCore",
|
||||
"-lswiftFoundation",
|
||||
"-lswiftObjectiveC",
|
||||
"-lswiftDarwin",
|
||||
"-lswiftDispatch",
|
||||
"-L/usr/lib/swift",
|
||||
"-Wl,-rpath,/usr/lib/swift",
|
||||
"-Wl,-rpath,@loader_path"
|
||||
]
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"action_name": "build_swift",
|
||||
"inputs": [
|
||||
"src/VirtualDisplay.swift",
|
||||
"src/Dummy.swift",
|
||||
"include/VirtualDisplayBridge.h"
|
||||
],
|
||||
"outputs": [
|
||||
"build_swift/libVirtualDisplay.dylib",
|
||||
"build_swift/virtual_display-Swift.h"
|
||||
],
|
||||
"action": [
|
||||
"swiftc",
|
||||
"src/VirtualDisplay.swift",
|
||||
"src/Dummy.swift",
|
||||
"-import-objc-header", "include/VirtualDisplayBridge.h",
|
||||
"-emit-objc-header-path", "./build_swift/virtual_display-Swift.h",
|
||||
"-emit-library", "-o", "./build_swift/libVirtualDisplay.dylib",
|
||||
"-emit-module", "-module-name", "virtual_display",
|
||||
"-module-link-name", "VirtualDisplay"
|
||||
]
|
||||
},
|
||||
{
|
||||
"action_name": "copy_swift_lib",
|
||||
"inputs": [
|
||||
"<(module_root_dir)/build_swift/libVirtualDisplay.dylib"
|
||||
],
|
||||
"outputs": [
|
||||
"<(PRODUCT_DIR)/libVirtualDisplay.dylib"
|
||||
],
|
||||
"action": [
|
||||
"sh",
|
||||
"-c",
|
||||
"cp -f <(module_root_dir)/build_swift/libVirtualDisplay.dylib <(PRODUCT_DIR)/libVirtualDisplay.dylib && install_name_tool -id @rpath/libVirtualDisplay.dylib <(PRODUCT_DIR)/libVirtualDisplay.dylib"
|
||||
]
|
||||
}
|
||||
]
|
||||
}]
|
||||
]
|
||||
}]
|
||||
}
|
||||
121
spec/fixtures/native-addon/virtual-display/include/VirtualDisplayBridge.h
vendored
Normal file
121
spec/fixtures/native-addon/virtual-display/include/VirtualDisplayBridge.h
vendored
Normal file
@@ -0,0 +1,121 @@
|
||||
#ifndef VirtualDisplayBridge_h
|
||||
#define VirtualDisplayBridge_h
|
||||
|
||||
#import <CoreGraphics/CoreGraphics.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface VirtualDisplayBridge : NSObject
|
||||
|
||||
+ (NSInteger)create:(int)width height:(int)height x:(int)x y:(int)y;
|
||||
+ (BOOL)destroy:(NSInteger)displayId;
|
||||
+ (BOOL)forceCleanup;
|
||||
|
||||
@end
|
||||
|
||||
@interface CGVirtualDisplay : NSObject {
|
||||
unsigned int _vendorID;
|
||||
unsigned int _productID;
|
||||
unsigned int _serialNum;
|
||||
NSString* _name;
|
||||
struct CGSize _sizeInMillimeters;
|
||||
unsigned int _maxPixelsWide;
|
||||
unsigned int _maxPixelsHigh;
|
||||
struct CGPoint _redPrimary;
|
||||
struct CGPoint _greenPrimary;
|
||||
struct CGPoint _bluePrimary;
|
||||
struct CGPoint _whitePoint;
|
||||
id _queue;
|
||||
id _terminationHandler;
|
||||
void* _client;
|
||||
unsigned int _displayID;
|
||||
unsigned int _hiDPI;
|
||||
NSArray* _modes;
|
||||
unsigned int _serverRPC_port;
|
||||
unsigned int _proxyRPC_port;
|
||||
unsigned int _clientHandler_port;
|
||||
}
|
||||
|
||||
@property(readonly, nonatomic) NSArray* modes;
|
||||
@property(readonly, nonatomic) unsigned int hiDPI;
|
||||
@property(readonly, nonatomic) unsigned int displayID;
|
||||
@property(readonly, nonatomic) id terminationHandler;
|
||||
@property(readonly, nonatomic) id queue;
|
||||
@property(readonly, nonatomic) struct CGPoint whitePoint;
|
||||
@property(readonly, nonatomic) struct CGPoint bluePrimary;
|
||||
@property(readonly, nonatomic) struct CGPoint greenPrimary;
|
||||
@property(readonly, nonatomic) struct CGPoint redPrimary;
|
||||
@property(readonly, nonatomic) unsigned int maxPixelsHigh;
|
||||
@property(readonly, nonatomic) unsigned int maxPixelsWide;
|
||||
@property(readonly, nonatomic) struct CGSize sizeInMillimeters;
|
||||
@property(readonly, nonatomic) NSString* name;
|
||||
@property(readonly, nonatomic) unsigned int serialNum;
|
||||
@property(readonly, nonatomic) unsigned int productID;
|
||||
@property(readonly, nonatomic) unsigned int vendorID;
|
||||
- (BOOL)applySettings:(id)arg1;
|
||||
- (void)dealloc;
|
||||
- (id)initWithDescriptor:(id)arg1;
|
||||
|
||||
@end
|
||||
|
||||
@interface CGVirtualDisplayDescriptor : NSObject {
|
||||
unsigned int _vendorID;
|
||||
unsigned int _productID;
|
||||
unsigned int _serialNum;
|
||||
NSString* _name;
|
||||
struct CGSize _sizeInMillimeters;
|
||||
unsigned int _maxPixelsWide;
|
||||
unsigned int _maxPixelsHigh;
|
||||
struct CGPoint _redPrimary;
|
||||
struct CGPoint _greenPrimary;
|
||||
struct CGPoint _bluePrimary;
|
||||
struct CGPoint _whitePoint;
|
||||
id _queue;
|
||||
id _terminationHandler;
|
||||
}
|
||||
|
||||
@property(retain, nonatomic) id queue;
|
||||
@property(retain, nonatomic) NSString* name;
|
||||
@property(nonatomic) struct CGPoint whitePoint;
|
||||
@property(nonatomic) struct CGPoint bluePrimary;
|
||||
@property(nonatomic) struct CGPoint greenPrimary;
|
||||
@property(nonatomic) struct CGPoint redPrimary;
|
||||
@property(nonatomic) unsigned int maxPixelsHigh;
|
||||
@property(nonatomic) unsigned int maxPixelsWide;
|
||||
@property(nonatomic) struct CGSize sizeInMillimeters;
|
||||
@property(nonatomic) unsigned int serialNum;
|
||||
@property(nonatomic) unsigned int productID;
|
||||
@property(nonatomic) unsigned int vendorID;
|
||||
- (void)dealloc;
|
||||
- (id)init;
|
||||
@property(copy, nonatomic) id terminationHandler;
|
||||
|
||||
@end
|
||||
|
||||
@interface CGVirtualDisplayMode : NSObject {
|
||||
unsigned int _width;
|
||||
unsigned int _height;
|
||||
double _refreshRate;
|
||||
}
|
||||
|
||||
@property(readonly, nonatomic) double refreshRate;
|
||||
@property(readonly, nonatomic) unsigned int height;
|
||||
@property(readonly, nonatomic) unsigned int width;
|
||||
- (id)initWithWidth:(unsigned int)arg1
|
||||
height:(unsigned int)arg2
|
||||
refreshRate:(double)arg3;
|
||||
|
||||
@end
|
||||
|
||||
@interface CGVirtualDisplaySettings : NSObject {
|
||||
NSArray* _modes;
|
||||
unsigned int _hiDPI;
|
||||
}
|
||||
|
||||
@property(nonatomic) unsigned int hiDPI;
|
||||
- (void)dealloc;
|
||||
- (id)init;
|
||||
@property(retain, nonatomic) NSArray* modes;
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
7
spec/fixtures/native-addon/virtual-display/lib/virtual-display.js
vendored
Normal file
7
spec/fixtures/native-addon/virtual-display/lib/virtual-display.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = process.platform === 'darwin'
|
||||
? require('../build/Release/virtual_display.node')
|
||||
: {
|
||||
create: () => { throw new Error('Virtual displays only supported on macOS'); },
|
||||
destroy: () => { throw new Error('Virtual displays only supported on macOS'); },
|
||||
forceCleanup: () => { throw new Error('Virtual displays only supported on macOS'); }
|
||||
};
|
||||
20
spec/fixtures/native-addon/virtual-display/package.json
vendored
Normal file
20
spec/fixtures/native-addon/virtual-display/package.json
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "@electron-ci/virtual-display",
|
||||
"version": "1.0.0",
|
||||
"description": "Virtual display for multi-monitor testing",
|
||||
"main": "./lib/virtual-display.js",
|
||||
"scripts": {
|
||||
"clean": "rm -rf build",
|
||||
"build-electron": "electron-rebuild",
|
||||
"build": "node-gyp configure && node-gyp build"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0",
|
||||
"node-addon-api": "^8.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^30.0.0",
|
||||
"node-gyp": "^11.1.0"
|
||||
}
|
||||
}
|
||||
181
spec/fixtures/native-addon/virtual-display/src/Dummy.swift
vendored
Normal file
181
spec/fixtures/native-addon/virtual-display/src/Dummy.swift
vendored
Normal file
@@ -0,0 +1,181 @@
|
||||
import Foundation
|
||||
import Cocoa
|
||||
import os.log
|
||||
|
||||
class DummyManager {
|
||||
struct DefinedDummy {
|
||||
var dummy: Dummy
|
||||
}
|
||||
|
||||
static var definedDummies: [Int: DefinedDummy] = [:]
|
||||
static var dummyCounter: Int = 0
|
||||
|
||||
static func createDummy(_ dummyDefinition: DummyDefinition, isPortrait _: Bool = false, serialNum: UInt32 = 0, doConnect: Bool = true) -> Int? {
|
||||
let dummy = Dummy(dummyDefinition: dummyDefinition, serialNum: serialNum, doConnect: doConnect)
|
||||
|
||||
if !dummy.isConnected {
|
||||
print("[DummyManager.createDummy:\(#line)] Failed to create virtual display - not connected")
|
||||
return nil
|
||||
}
|
||||
self.dummyCounter += 1
|
||||
self.definedDummies[self.dummyCounter] = DefinedDummy(dummy: dummy)
|
||||
return self.dummyCounter
|
||||
}
|
||||
|
||||
static func discardDummyByNumber(_ number: Int) {
|
||||
if let definedDummy = self.definedDummies[number] {
|
||||
if definedDummy.dummy.isConnected {
|
||||
definedDummy.dummy.disconnect()
|
||||
}
|
||||
}
|
||||
self.definedDummies[number] = nil
|
||||
}
|
||||
|
||||
static func forceCleanup() {
|
||||
for (_, definedDummy) in self.definedDummies {
|
||||
if definedDummy.dummy.isConnected {
|
||||
definedDummy.dummy.virtualDisplay = nil
|
||||
definedDummy.dummy.displayIdentifier = 0
|
||||
definedDummy.dummy.isConnected = false
|
||||
}
|
||||
}
|
||||
|
||||
self.definedDummies.removeAll()
|
||||
self.dummyCounter = 0
|
||||
|
||||
var config: CGDisplayConfigRef? = nil
|
||||
if CGBeginDisplayConfiguration(&config) == .success {
|
||||
CGCompleteDisplayConfiguration(config, .permanently)
|
||||
}
|
||||
|
||||
usleep(2000000)
|
||||
|
||||
if CGBeginDisplayConfiguration(&config) == .success {
|
||||
CGCompleteDisplayConfiguration(config, .forSession)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DummyDefinition {
|
||||
let aspectWidth, aspectHeight, multiplierStep, minMultiplier, maxMultiplier: Int
|
||||
let refreshRates: [Double]
|
||||
let description: String
|
||||
let addSeparatorAfter: Bool
|
||||
|
||||
init(_ aspectWidth: Int, _ aspectHeight: Int, _ step: Int, _ refreshRates: [Double], _ description: String, _ addSeparatorAfter: Bool = false) {
|
||||
let minX: Int = 720
|
||||
let minY: Int = 720
|
||||
let maxX: Int = 8192
|
||||
let maxY: Int = 8192
|
||||
let minMultiplier = max(Int(ceil(Float(minX) / (Float(aspectWidth) * Float(step)))), Int(ceil(Float(minY) / (Float(aspectHeight) * Float(step)))))
|
||||
let maxMultiplier = min(Int(floor(Float(maxX) / (Float(aspectWidth) * Float(step)))), Int(floor(Float(maxY) / (Float(aspectHeight) * Float(step)))))
|
||||
|
||||
self.aspectWidth = aspectWidth
|
||||
self.aspectHeight = aspectHeight
|
||||
self.minMultiplier = minMultiplier
|
||||
self.maxMultiplier = maxMultiplier
|
||||
self.multiplierStep = step
|
||||
self.refreshRates = refreshRates
|
||||
self.description = description
|
||||
self.addSeparatorAfter = addSeparatorAfter
|
||||
}
|
||||
}
|
||||
|
||||
class Dummy: Equatable {
|
||||
var virtualDisplay: CGVirtualDisplay?
|
||||
var dummyDefinition: DummyDefinition
|
||||
let serialNum: UInt32
|
||||
var isConnected: Bool = false
|
||||
var displayIdentifier: CGDirectDisplayID = 0
|
||||
|
||||
static func == (lhs: Dummy, rhs: Dummy) -> Bool {
|
||||
lhs.serialNum == rhs.serialNum
|
||||
}
|
||||
|
||||
init(dummyDefinition: DummyDefinition, serialNum: UInt32 = 0, doConnect: Bool = true) {
|
||||
var storedSerialNum: UInt32 = serialNum
|
||||
if storedSerialNum == 0 {
|
||||
storedSerialNum = UInt32.random(in: 0 ... UInt32.max)
|
||||
}
|
||||
self.dummyDefinition = dummyDefinition
|
||||
self.serialNum = storedSerialNum
|
||||
if doConnect {
|
||||
_ = self.connect()
|
||||
}
|
||||
}
|
||||
|
||||
func getName() -> String {
|
||||
"Dummy \(self.dummyDefinition.description.components(separatedBy: " ").first ?? self.dummyDefinition.description)"
|
||||
}
|
||||
|
||||
func connect() -> Bool {
|
||||
if self.virtualDisplay != nil || self.isConnected {
|
||||
self.disconnect()
|
||||
}
|
||||
let name: String = self.getName()
|
||||
if let virtualDisplay = Dummy.createVirtualDisplay(self.dummyDefinition, name: name, serialNum: self.serialNum) {
|
||||
self.virtualDisplay = virtualDisplay
|
||||
self.displayIdentifier = virtualDisplay.displayID
|
||||
self.isConnected = true
|
||||
print("[Dummy.connect:\(#line)] Successfully connected virtual display: \(name)")
|
||||
return true
|
||||
} else {
|
||||
print("[Dummy.connect:\(#line)] Failed to connect virtual display: \(name)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func disconnect() {
|
||||
self.virtualDisplay = nil
|
||||
self.isConnected = false
|
||||
print("[Dummy.disconnect:\(#line)] Disconnected virtual display: \(self.getName())")
|
||||
}
|
||||
|
||||
private static func waitForDisplayRegistration(_ displayId: CGDirectDisplayID) -> Bool {
|
||||
for _ in 0..<20 {
|
||||
var count: UInt32 = 0, displays = [CGDirectDisplayID](repeating: 0, count: 32)
|
||||
if CGGetActiveDisplayList(32, &displays, &count) == .success && displays[0..<Int(count)].contains(displayId) {
|
||||
return true
|
||||
}
|
||||
usleep(100000)
|
||||
}
|
||||
print("[Dummy.waitForDisplayRegistration:\(#line)] Failed to register virtual display: \(displayId)")
|
||||
return false
|
||||
}
|
||||
|
||||
static func createVirtualDisplay(_ definition: DummyDefinition, name: String, serialNum: UInt32, hiDPI: Bool = false) -> CGVirtualDisplay? {
|
||||
if let descriptor = CGVirtualDisplayDescriptor() {
|
||||
descriptor.queue = DispatchQueue.global(qos: .userInteractive)
|
||||
descriptor.name = name
|
||||
descriptor.whitePoint = CGPoint(x: 0.950, y: 1.000)
|
||||
descriptor.redPrimary = CGPoint(x: 0.454, y: 0.242)
|
||||
descriptor.greenPrimary = CGPoint(x: 0.353, y: 0.674)
|
||||
descriptor.bluePrimary = CGPoint(x: 0.157, y: 0.084)
|
||||
descriptor.maxPixelsWide = UInt32(definition.aspectWidth * definition.multiplierStep * definition.maxMultiplier)
|
||||
descriptor.maxPixelsHigh = UInt32(definition.aspectHeight * definition.multiplierStep * definition.maxMultiplier)
|
||||
let diagonalSizeRatio: Double = (24 * 25.4) / sqrt(Double(definition.aspectWidth * definition.aspectWidth + definition.aspectHeight * definition.aspectHeight))
|
||||
descriptor.sizeInMillimeters = CGSize(width: Double(definition.aspectWidth) * diagonalSizeRatio, height: Double(definition.aspectHeight) * diagonalSizeRatio)
|
||||
descriptor.serialNum = serialNum
|
||||
descriptor.productID = UInt32(min(definition.aspectWidth - 1, 255) * 256 + min(definition.aspectHeight - 1, 255))
|
||||
descriptor.vendorID = UInt32(0xF0F0)
|
||||
if let display = CGVirtualDisplay(descriptor: descriptor) {
|
||||
var modes = [CGVirtualDisplayMode?](repeating: nil, count: definition.maxMultiplier - definition.minMultiplier + 1)
|
||||
for multiplier in definition.minMultiplier ... definition.maxMultiplier {
|
||||
for refreshRate in definition.refreshRates {
|
||||
let width = UInt32(definition.aspectWidth * multiplier * definition.multiplierStep)
|
||||
let height = UInt32(definition.aspectHeight * multiplier * definition.multiplierStep)
|
||||
modes[multiplier - definition.minMultiplier] = CGVirtualDisplayMode(width: width, height: height, refreshRate: refreshRate)!
|
||||
}
|
||||
}
|
||||
if let settings = CGVirtualDisplaySettings() {
|
||||
settings.hiDPI = hiDPI ? 1 : 0
|
||||
settings.modes = modes as [Any]
|
||||
if display.applySettings(settings) {
|
||||
return waitForDisplayRegistration(display.displayID) ? display : nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
59
spec/fixtures/native-addon/virtual-display/src/VirtualDisplay.swift
vendored
Normal file
59
spec/fixtures/native-addon/virtual-display/src/VirtualDisplay.swift
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
import Foundation
|
||||
import Cocoa
|
||||
import os.log
|
||||
|
||||
@objc public class VirtualDisplay: NSObject {
|
||||
@objc public static func create(width: Int, height: Int, x: Int, y: Int) -> Int {
|
||||
let refreshRates: [Double] = [60.0] // Always 60Hz default
|
||||
let description = "\(width)x\(height) Display"
|
||||
let definition = DummyDefinition(width, height, 1, refreshRates, description, false)
|
||||
let displayId = DummyManager.createDummy(definition) ?? 0
|
||||
positionDisplay(displayId: displayId, x: x, y: y)
|
||||
|
||||
return displayId
|
||||
}
|
||||
|
||||
@objc public static func destroy(id: Int) -> Bool {
|
||||
DummyManager.discardDummyByNumber(id)
|
||||
return true
|
||||
}
|
||||
|
||||
@objc public static func forceCleanup() -> Bool {
|
||||
DummyManager.forceCleanup()
|
||||
return true
|
||||
}
|
||||
|
||||
private static func positionDisplay(displayId: Int, x: Int, y: Int) {
|
||||
guard let definedDummy = DummyManager.definedDummies[displayId],
|
||||
definedDummy.dummy.isConnected else {
|
||||
os_log("VirtualDisplay: Cannot position display %{public}@: display not found or not connected", type: .error, "\(displayId)")
|
||||
return
|
||||
}
|
||||
|
||||
let cgDisplayId = definedDummy.dummy.displayIdentifier
|
||||
|
||||
var config: CGDisplayConfigRef? = nil
|
||||
let beginResult = CGBeginDisplayConfiguration(&config)
|
||||
|
||||
if beginResult != .success {
|
||||
os_log("VirtualDisplay: Cannot position display, failed to begin display configuration via CGBeginDisplayConfiguration: error %{public}@", type: .error, "\(beginResult.rawValue)")
|
||||
return
|
||||
}
|
||||
|
||||
let configResult = CGConfigureDisplayOrigin(config, cgDisplayId, Int32(x), Int32(y))
|
||||
|
||||
if configResult != .success {
|
||||
os_log("VirtualDisplay: Cannot position display, failed to configure display origin via CGConfigureDisplayOrigin: error %{public}@", type: .error, "\(configResult.rawValue)")
|
||||
CGCancelDisplayConfiguration(config)
|
||||
return
|
||||
}
|
||||
|
||||
let completeResult = CGCompleteDisplayConfiguration(config, .permanently)
|
||||
|
||||
if completeResult == .success {
|
||||
os_log("VirtualDisplay: Successfully positioned display %{public}@ at (%{public}@, %{public}@)", type: .info, "\(displayId)", "\(x)", "\(y)")
|
||||
} else {
|
||||
os_log("VirtualDisplay: Cannot position display, failed to complete display configuration via CGCompleteDisplayConfiguration: error %{public}@", type: .error, "\(completeResult.rawValue)")
|
||||
}
|
||||
}
|
||||
}
|
||||
18
spec/fixtures/native-addon/virtual-display/src/VirtualDisplayBridge.m
vendored
Normal file
18
spec/fixtures/native-addon/virtual-display/src/VirtualDisplayBridge.m
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
#import "VirtualDisplayBridge.h"
|
||||
#import "../build_swift/virtual_display-Swift.h"
|
||||
|
||||
@implementation VirtualDisplayBridge
|
||||
|
||||
+ (NSInteger)create:(int)width height:(int)height x:(int)x y:(int)y {
|
||||
return [VirtualDisplay createWithWidth:width height:height x:x y:y];
|
||||
}
|
||||
|
||||
+ (BOOL)destroy:(NSInteger)displayId {
|
||||
return [VirtualDisplay destroyWithId:(int)displayId];
|
||||
}
|
||||
|
||||
+ (BOOL)forceCleanup {
|
||||
return [VirtualDisplay forceCleanup];
|
||||
}
|
||||
|
||||
@end
|
||||
197
spec/fixtures/native-addon/virtual-display/src/addon.mm
vendored
Normal file
197
spec/fixtures/native-addon/virtual-display/src/addon.mm
vendored
Normal file
@@ -0,0 +1,197 @@
|
||||
#include <js_native_api.h>
|
||||
#include <node_api.h>
|
||||
#include "VirtualDisplayBridge.h"
|
||||
|
||||
namespace {
|
||||
|
||||
typedef struct {
|
||||
const char* name;
|
||||
int default_val;
|
||||
int* ptr;
|
||||
} PropertySpec;
|
||||
|
||||
// Helper function to get an integer property from an object
|
||||
bool GetIntProperty(napi_env env,
|
||||
napi_value object,
|
||||
const char* prop_name,
|
||||
int* result,
|
||||
int default_value) {
|
||||
*result = default_value;
|
||||
|
||||
bool has_prop;
|
||||
if (napi_has_named_property(env, object, prop_name, &has_prop) != napi_ok ||
|
||||
!has_prop) {
|
||||
return true;
|
||||
}
|
||||
|
||||
napi_value prop_value;
|
||||
if (napi_get_named_property(env, object, prop_name, &prop_value) != napi_ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (napi_get_value_int32(env, prop_value, result) != napi_ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Helper function to validate and parse object properties
|
||||
bool ParseObjectProperties(napi_env env,
|
||||
napi_value object,
|
||||
PropertySpec props[],
|
||||
size_t prop_count) {
|
||||
// Process all properties
|
||||
for (size_t i = 0; i < prop_count; i++) {
|
||||
if (!GetIntProperty(env, object, props[i].name, props[i].ptr,
|
||||
props[i].default_val)) {
|
||||
char error_msg[50];
|
||||
snprintf(error_msg, sizeof(error_msg), "%s must be a number",
|
||||
props[i].name);
|
||||
napi_throw_error(env, NULL, error_msg);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for unknown properties
|
||||
napi_value prop_names;
|
||||
uint32_t count;
|
||||
napi_get_property_names(env, object, &prop_names);
|
||||
napi_get_array_length(env, prop_names, &count);
|
||||
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
napi_value prop_name;
|
||||
napi_get_element(env, prop_names, i, &prop_name);
|
||||
size_t len;
|
||||
char name[20];
|
||||
napi_get_value_string_utf8(env, prop_name, name, sizeof(name), &len);
|
||||
|
||||
bool found = false;
|
||||
for (size_t j = 0; j < prop_count; j++) {
|
||||
if (strcmp(name, props[j].name) == 0) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
napi_throw_error(env, NULL, "Object contains unknown properties");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// virtualDisplay.create()
|
||||
napi_value create(napi_env env, napi_callback_info info) {
|
||||
size_t argc = 1;
|
||||
napi_value args[1];
|
||||
|
||||
if (napi_get_cb_info(env, info, &argc, args, NULL, NULL) != napi_ok) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int width = 1920, height = 1080, x = 0, y = 0;
|
||||
|
||||
PropertySpec props[] = {{"width", 1920, &width},
|
||||
{"height", 1080, &height},
|
||||
{"x", 0, &x},
|
||||
{"y", 0, &y}};
|
||||
|
||||
if (argc >= 1) {
|
||||
napi_valuetype valuetype;
|
||||
if (napi_typeof(env, args[0], &valuetype) != napi_ok) {
|
||||
napi_throw_error(env, NULL, "Failed to get argument type");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (valuetype == napi_object) {
|
||||
if (!ParseObjectProperties(env, args[0], props,
|
||||
sizeof(props) / sizeof(props[0]))) {
|
||||
return NULL;
|
||||
}
|
||||
} else {
|
||||
napi_throw_error(env, NULL, "Expected an object as the argument");
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
NSInteger displayId = [VirtualDisplayBridge create:width
|
||||
height:height
|
||||
x:x
|
||||
y:y];
|
||||
|
||||
if (displayId == 0) {
|
||||
napi_throw_error(env, NULL, "Failed to create virtual display");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
napi_value result;
|
||||
if (napi_create_int64(env, displayId, &result) != napi_ok) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// virtualDisplay.forceCleanup()
|
||||
napi_value forceCleanup(napi_env env, napi_callback_info info) {
|
||||
BOOL result = [VirtualDisplayBridge forceCleanup];
|
||||
|
||||
napi_value js_result;
|
||||
if (napi_get_boolean(env, result, &js_result) != napi_ok) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return js_result;
|
||||
}
|
||||
|
||||
// virtualDisplay.destroy()
|
||||
napi_value destroy(napi_env env, napi_callback_info info) {
|
||||
size_t argc = 1;
|
||||
napi_value args[1];
|
||||
|
||||
if (napi_get_cb_info(env, info, &argc, args, NULL, NULL) != napi_ok) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (argc < 1) {
|
||||
napi_throw_error(env, NULL, "Expected number argument");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int64_t displayId;
|
||||
if (napi_get_value_int64(env, args[0], &displayId) != napi_ok) {
|
||||
napi_throw_error(env, NULL, "Expected number argument");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
BOOL result = [VirtualDisplayBridge destroy:(NSInteger)displayId];
|
||||
|
||||
napi_value js_result;
|
||||
if (napi_get_boolean(env, result, &js_result) != napi_ok) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return js_result;
|
||||
}
|
||||
|
||||
napi_value Init(napi_env env, napi_value exports) {
|
||||
napi_property_descriptor descriptors[] = {
|
||||
{"create", NULL, create, NULL, NULL, NULL, napi_default, NULL},
|
||||
{"destroy", NULL, destroy, NULL, NULL, NULL, napi_default, NULL},
|
||||
{"forceCleanup", NULL, forceCleanup, NULL, NULL, NULL, napi_default,
|
||||
NULL}};
|
||||
|
||||
if (napi_define_properties(env, exports,
|
||||
sizeof(descriptors) / sizeof(*descriptors),
|
||||
descriptors) != napi_ok) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return exports;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
|
||||
@@ -186,6 +186,39 @@ describe('webContents.setWindowOpenHandler', () => {
|
||||
await once(browserWindow.webContents, 'did-create-window');
|
||||
});
|
||||
|
||||
it('reuses an existing window when window.open is called with the same frame name', async () => {
|
||||
let handlerCallCount = 0;
|
||||
browserWindow.webContents.setWindowOpenHandler(() => {
|
||||
handlerCallCount++;
|
||||
return { action: 'allow' };
|
||||
});
|
||||
|
||||
const didCreateWindow = once(browserWindow.webContents, 'did-create-window') as Promise<[BrowserWindow, Electron.DidCreateWindowDetails]>;
|
||||
await browserWindow.webContents.executeJavaScript("window.open('about:blank?one', 'named-target', 'show=no') && true");
|
||||
const [childWindow] = await didCreateWindow;
|
||||
expect(handlerCallCount).to.equal(1);
|
||||
expect(childWindow.webContents.getURL()).to.equal('about:blank?one');
|
||||
|
||||
browserWindow.webContents.on('did-create-window', () => {
|
||||
assert.fail('did-create-window should not fire when reusing a named window');
|
||||
});
|
||||
|
||||
const didNavigate = once(childWindow.webContents, 'did-navigate');
|
||||
const sameWindow = await browserWindow.webContents.executeJavaScript(`
|
||||
(() => {
|
||||
const first = window.open('about:blank?one', 'named-target', 'show=no');
|
||||
const second = window.open('about:blank?two', 'named-target', 'show=no');
|
||||
return first === second;
|
||||
})()
|
||||
`);
|
||||
await didNavigate;
|
||||
|
||||
expect(sameWindow).to.be.true('window.open with matching frame name should return the same window proxy');
|
||||
expect(handlerCallCount).to.equal(1, 'setWindowOpenHandler should not be called when Blink resolves the named target');
|
||||
expect(childWindow.webContents.getURL()).to.equal('about:blank?two');
|
||||
expect(BrowserWindow.getAllWindows()).to.have.lengthOf(2);
|
||||
});
|
||||
|
||||
it('can change webPreferences of child windows', async () => {
|
||||
browserWindow.webContents.setWindowOpenHandler(() => ({ action: 'allow', overrideBrowserWindowOptions: { webPreferences: { defaultFontSize: 30 } } }));
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"@electron-ci/is-valid-window": "*",
|
||||
"@electron-ci/osr-gpu": "*",
|
||||
"@electron-ci/uv-dlopen": "*",
|
||||
"@electron-ci/virtual-display": "*",
|
||||
"@electron/fuses": "^1.8.0",
|
||||
"@electron/packager": "^18.3.2",
|
||||
"@types/basic-auth": "^1.1.8",
|
||||
|
||||
311
yarn.lock
311
yarn.lock
@@ -345,6 +345,24 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/code-frame@npm:^7.27.1":
|
||||
version: 7.29.0
|
||||
resolution: "@babel/code-frame@npm:7.29.0"
|
||||
dependencies:
|
||||
"@babel/helper-validator-identifier": "npm:^7.28.5"
|
||||
js-tokens: "npm:^4.0.0"
|
||||
picocolors: "npm:^1.1.1"
|
||||
checksum: 10c0/d34cc504e7765dfb576a663d97067afb614525806b5cad1a5cc1a7183b916fec8ff57fa233585e3926fd5a9e6b31aae6df91aa81ae9775fb7a28f658d3346f0d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-validator-identifier@npm:^7.28.5":
|
||||
version: 7.28.5
|
||||
resolution: "@babel/helper-validator-identifier@npm:7.28.5"
|
||||
checksum: 10c0/42aaebed91f739a41f3d80b72752d1f95fd7c72394e8e4bd7cdd88817e0774d80a432451bcba17c2c642c257c483bf1d409dd4548883429ea9493a3bc4ab0847
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@datadog/datadog-ci-base@npm:4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "@datadog/datadog-ci-base@npm:4.1.2"
|
||||
@@ -648,6 +666,17 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@electron-ci/virtual-display@npm:*, @electron-ci/virtual-display@workspace:spec/fixtures/native-addon/virtual-display":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@electron-ci/virtual-display@workspace:spec/fixtures/native-addon/virtual-display"
|
||||
dependencies:
|
||||
"@types/jest": "npm:^30.0.0"
|
||||
bindings: "npm:^1.5.0"
|
||||
node-addon-api: "npm:^8.3.0"
|
||||
node-gyp: "npm:^11.1.0"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@electron/asar@npm:^3.2.13":
|
||||
version: 3.2.13
|
||||
resolution: "@electron/asar@npm:3.2.13"
|
||||
@@ -1136,6 +1165,63 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@jest/diff-sequences@npm:30.3.0":
|
||||
version: 30.3.0
|
||||
resolution: "@jest/diff-sequences@npm:30.3.0"
|
||||
checksum: 10c0/8922c16a869b839b6c05f677023b3e5a9aa1610ad78a9c5ec8bd6654e35e8136ea1c7b60ad561910e2ad964bfdb0b09b0254ff8dcfacd4562095766f60c63d76
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@jest/expect-utils@npm:30.3.0":
|
||||
version: 30.3.0
|
||||
resolution: "@jest/expect-utils@npm:30.3.0"
|
||||
dependencies:
|
||||
"@jest/get-type": "npm:30.1.0"
|
||||
checksum: 10c0/4bb60fb434cb8ed325735bd39171b61621e110502ecc502089805d203ecb17b9fc5a400aeffb83b41fabcc819628a9c38c955f90a716d6aaff193d10926fc854
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@jest/get-type@npm:30.1.0":
|
||||
version: 30.1.0
|
||||
resolution: "@jest/get-type@npm:30.1.0"
|
||||
checksum: 10c0/3e65fd5015f551c51ec68fca31bbd25b466be0e8ee8075d9610fa1c686ea1e70a942a0effc7b10f4ea9a338c24337e1ad97ff69d3ebacc4681b7e3e80d1b24ac
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@jest/pattern@npm:30.0.1":
|
||||
version: 30.0.1
|
||||
resolution: "@jest/pattern@npm:30.0.1"
|
||||
dependencies:
|
||||
"@types/node": "npm:*"
|
||||
jest-regex-util: "npm:30.0.1"
|
||||
checksum: 10c0/32c5a7bfb6c591f004dac0ed36d645002ed168971e4c89bd915d1577031672870032594767557b855c5bc330aa1e39a2f54bf150d2ee88a7a0886e9cb65318bc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@jest/schemas@npm:30.0.5":
|
||||
version: 30.0.5
|
||||
resolution: "@jest/schemas@npm:30.0.5"
|
||||
dependencies:
|
||||
"@sinclair/typebox": "npm:^0.34.0"
|
||||
checksum: 10c0/449dcd7ec5c6505e9ac3169d1143937e67044ae3e66a729ce4baf31812dfd30535f2b3b2934393c97cfdf5984ff581120e6b38f62b8560c8b5b7cc07f4175f65
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@jest/types@npm:30.3.0":
|
||||
version: 30.3.0
|
||||
resolution: "@jest/types@npm:30.3.0"
|
||||
dependencies:
|
||||
"@jest/pattern": "npm:30.0.1"
|
||||
"@jest/schemas": "npm:30.0.5"
|
||||
"@types/istanbul-lib-coverage": "npm:^2.0.6"
|
||||
"@types/istanbul-reports": "npm:^3.0.4"
|
||||
"@types/node": "npm:*"
|
||||
"@types/yargs": "npm:^17.0.33"
|
||||
chalk: "npm:^4.1.2"
|
||||
checksum: 10c0/c3e3f4de0b77a7ced345f47d3687b1094c1b6c1521529a7ca66a76f9a80194f79179a1dbc32d6761a5b67914a8f78be1e65d1408107efcb1f252c4a63b5ddd92
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@jridgewell/gen-mapping@npm:^0.3.5":
|
||||
version: 0.3.5
|
||||
resolution: "@jridgewell/gen-mapping@npm:0.3.5"
|
||||
@@ -1684,6 +1770,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sinclair/typebox@npm:^0.34.0":
|
||||
version: 0.34.48
|
||||
resolution: "@sinclair/typebox@npm:0.34.48"
|
||||
checksum: 10c0/e09f26d8ad471a07ee64004eea7c4ec185349a1f61c03e87e71ea33cbe98e97959940076c2e52968a955ffd4c215bf5ba7035e77079511aac7935f25e989e29d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sindresorhus/is@npm:^4.0.0":
|
||||
version: 4.6.0
|
||||
resolution: "@sindresorhus/is@npm:4.6.0"
|
||||
@@ -1963,6 +2056,41 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.6":
|
||||
version: 2.0.6
|
||||
resolution: "@types/istanbul-lib-coverage@npm:2.0.6"
|
||||
checksum: 10c0/3948088654f3eeb45363f1db158354fb013b362dba2a5c2c18c559484d5eb9f6fd85b23d66c0a7c2fcfab7308d0a585b14dadaca6cc8bf89ebfdc7f8f5102fb7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/istanbul-lib-report@npm:*":
|
||||
version: 3.0.3
|
||||
resolution: "@types/istanbul-lib-report@npm:3.0.3"
|
||||
dependencies:
|
||||
"@types/istanbul-lib-coverage": "npm:*"
|
||||
checksum: 10c0/247e477bbc1a77248f3c6de5dadaae85ff86ac2d76c5fc6ab1776f54512a745ff2a5f791d22b942e3990ddbd40f3ef5289317c4fca5741bedfaa4f01df89051c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/istanbul-reports@npm:^3.0.4":
|
||||
version: 3.0.4
|
||||
resolution: "@types/istanbul-reports@npm:3.0.4"
|
||||
dependencies:
|
||||
"@types/istanbul-lib-report": "npm:*"
|
||||
checksum: 10c0/1647fd402aced5b6edac87274af14ebd6b3a85447ef9ad11853a70fd92a98d35f81a5d3ea9fcb5dbb5834e800c6e35b64475e33fcae6bfa9acc70d61497c54ee
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/jest@npm:^30.0.0":
|
||||
version: 30.0.0
|
||||
resolution: "@types/jest@npm:30.0.0"
|
||||
dependencies:
|
||||
expect: "npm:^30.0.0"
|
||||
pretty-format: "npm:^30.0.0"
|
||||
checksum: 10c0/20c6ce574154bc16f8dd6a97afacca4b8c4921a819496a3970382031c509ebe87a1b37b152a1b8475089b82d8ca951a9e95beb4b9bf78fbf579b1536f0b65969
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/json-buffer@npm:~3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "@types/json-buffer@npm:3.0.0"
|
||||
@@ -2197,6 +2325,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/stack-utils@npm:^2.0.3":
|
||||
version: 2.0.3
|
||||
resolution: "@types/stack-utils@npm:2.0.3"
|
||||
checksum: 10c0/1f4658385ae936330581bcb8aa3a066df03867d90281cdf89cc356d404bd6579be0f11902304e1f775d92df22c6dd761d4451c804b0a4fba973e06211e9bd77c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/stream-chain@npm:*":
|
||||
version: 2.0.0
|
||||
resolution: "@types/stream-chain@npm:2.0.0"
|
||||
@@ -2283,6 +2418,22 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/yargs-parser@npm:*":
|
||||
version: 21.0.3
|
||||
resolution: "@types/yargs-parser@npm:21.0.3"
|
||||
checksum: 10c0/e71c3bd9d0b73ca82e10bee2064c384ab70f61034bbfb78e74f5206283fc16a6d85267b606b5c22cb2a3338373586786fed595b2009825d6a9115afba36560a0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/yargs@npm:^17.0.33":
|
||||
version: 17.0.35
|
||||
resolution: "@types/yargs@npm:17.0.35"
|
||||
dependencies:
|
||||
"@types/yargs-parser": "npm:*"
|
||||
checksum: 10c0/609557826a6b85e73ccf587923f6429850d6dc70e420b455bab4601b670bfadf684b09ae288bccedab042c48ba65f1666133cf375814204b544009f57d6eef63
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/yauzl@npm:^2.9.1":
|
||||
version: 2.10.0
|
||||
resolution: "@types/yauzl@npm:2.10.0"
|
||||
@@ -2909,6 +3060,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ansi-styles@npm:^5.2.0":
|
||||
version: 5.2.0
|
||||
resolution: "ansi-styles@npm:5.2.0"
|
||||
checksum: 10c0/9c4ca80eb3c2fb7b33841c210d2f20807f40865d27008d7c3f707b7f95cab7d67462a565e2388ac3285b71cb3d9bb2173de8da37c57692a362885ec34d6e27df
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ansi-styles@npm:^6.0.0, ansi-styles@npm:^6.1.0, ansi-styles@npm:^6.2.1":
|
||||
version: 6.2.1
|
||||
resolution: "ansi-styles@npm:6.2.1"
|
||||
@@ -3356,7 +3514,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bindings@npm:^1.2.1":
|
||||
"bindings@npm:^1.2.1, bindings@npm:^1.5.0":
|
||||
version: 1.5.0
|
||||
resolution: "bindings@npm:1.5.0"
|
||||
dependencies:
|
||||
@@ -3769,7 +3927,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chalk@npm:^4.1.0, chalk@npm:^4.1.1":
|
||||
"chalk@npm:^4.1.0, chalk@npm:^4.1.1, chalk@npm:^4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "chalk@npm:4.1.2"
|
||||
dependencies:
|
||||
@@ -3921,6 +4079,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ci-info@npm:^4.2.0":
|
||||
version: 4.4.0
|
||||
resolution: "ci-info@npm:4.4.0"
|
||||
checksum: 10c0/44156201545b8dde01aa8a09ee2fe9fc7a73b1bef9adbd4606c9f61c8caeeb73fb7a575c88b0443f7b4edb5ee45debaa59ed54ba5f99698339393ca01349eb3a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cli-cursor@npm:^3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "cli-cursor@npm:3.1.0"
|
||||
@@ -4750,6 +4915,7 @@ __metadata:
|
||||
"@electron-ci/is-valid-window": "npm:*"
|
||||
"@electron-ci/osr-gpu": "npm:*"
|
||||
"@electron-ci/uv-dlopen": "npm:*"
|
||||
"@electron-ci/virtual-display": "npm:*"
|
||||
"@electron/fuses": "npm:^1.8.0"
|
||||
"@electron/packager": "npm:^18.3.2"
|
||||
"@types/basic-auth": "npm:^1.1.8"
|
||||
@@ -5285,6 +5451,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"escape-string-regexp@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "escape-string-regexp@npm:2.0.0"
|
||||
checksum: 10c0/2530479fe8db57eace5e8646c9c2a9c80fa279614986d16dcc6bcaceb63ae77f05a851ba6c43756d816c61d7f4534baf56e3c705e3e0d884818a46808811c507
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"escape-string-regexp@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "escape-string-regexp@npm:4.0.0"
|
||||
@@ -5845,6 +6018,20 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"expect@npm:^30.0.0":
|
||||
version: 30.3.0
|
||||
resolution: "expect@npm:30.3.0"
|
||||
dependencies:
|
||||
"@jest/expect-utils": "npm:30.3.0"
|
||||
"@jest/get-type": "npm:30.1.0"
|
||||
jest-matcher-utils: "npm:30.3.0"
|
||||
jest-message-util: "npm:30.3.0"
|
||||
jest-mock: "npm:30.3.0"
|
||||
jest-util: "npm:30.3.0"
|
||||
checksum: 10c0/a07a157a0c8b3f1e29bfe5ccbf03a3add2c69fe60d1af8a0980053bb6403d721d5f5e4616f1ea5833b747913f8c880c79ce4d98c23a71a2f0c27cf7273892576
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"exponential-backoff@npm:^3.1.1":
|
||||
version: 3.1.3
|
||||
resolution: "exponential-backoff@npm:3.1.3"
|
||||
@@ -8070,6 +8257,79 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jest-diff@npm:30.3.0":
|
||||
version: 30.3.0
|
||||
resolution: "jest-diff@npm:30.3.0"
|
||||
dependencies:
|
||||
"@jest/diff-sequences": "npm:30.3.0"
|
||||
"@jest/get-type": "npm:30.1.0"
|
||||
chalk: "npm:^4.1.2"
|
||||
pretty-format: "npm:30.3.0"
|
||||
checksum: 10c0/573a2a1a155b95fbde547d8ee33a5375179a8d03d4586025478dac16d695e4614aef075c3afa57e0f3a96cea8f638fa68a55c1e625f6e86b4f5b9e5850311ffb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jest-matcher-utils@npm:30.3.0":
|
||||
version: 30.3.0
|
||||
resolution: "jest-matcher-utils@npm:30.3.0"
|
||||
dependencies:
|
||||
"@jest/get-type": "npm:30.1.0"
|
||||
chalk: "npm:^4.1.2"
|
||||
jest-diff: "npm:30.3.0"
|
||||
pretty-format: "npm:30.3.0"
|
||||
checksum: 10c0/4c5f4b6435964110e64c4b5b42e3553fffe303ecdd68021147a7bcc72914aec3a899867c50db22b250c72aded53e3f7a9f64d83c9dca2e65ce27f36d23c6ca78
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jest-message-util@npm:30.3.0":
|
||||
version: 30.3.0
|
||||
resolution: "jest-message-util@npm:30.3.0"
|
||||
dependencies:
|
||||
"@babel/code-frame": "npm:^7.27.1"
|
||||
"@jest/types": "npm:30.3.0"
|
||||
"@types/stack-utils": "npm:^2.0.3"
|
||||
chalk: "npm:^4.1.2"
|
||||
graceful-fs: "npm:^4.2.11"
|
||||
picomatch: "npm:^4.0.3"
|
||||
pretty-format: "npm:30.3.0"
|
||||
slash: "npm:^3.0.0"
|
||||
stack-utils: "npm:^2.0.6"
|
||||
checksum: 10c0/6ce611caef76394872b23a111286b48e56f42655d14a5fbd0629d9b7437ed892e85ad96b15864bc22185c24ef670afb6665c57b9729458a36d50ffe8310f0926
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jest-mock@npm:30.3.0":
|
||||
version: 30.3.0
|
||||
resolution: "jest-mock@npm:30.3.0"
|
||||
dependencies:
|
||||
"@jest/types": "npm:30.3.0"
|
||||
"@types/node": "npm:*"
|
||||
jest-util: "npm:30.3.0"
|
||||
checksum: 10c0/9d95d550c6c998a85887c48ff5ee26de4bca18be91462ea8a8135d6023d591132465756f74981ca39b60f8708dfe38213a55bd4b619798a7b9438ca10d718099
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jest-regex-util@npm:30.0.1":
|
||||
version: 30.0.1
|
||||
resolution: "jest-regex-util@npm:30.0.1"
|
||||
checksum: 10c0/f30c70524ebde2d1012afe5ffa5691d5d00f7d5ba9e43d588f6460ac6fe96f9e620f2f9b36a02d0d3e7e77bc8efb8b3450ae3b80ac53c8be5099e01bf54f6728
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jest-util@npm:30.3.0":
|
||||
version: 30.3.0
|
||||
resolution: "jest-util@npm:30.3.0"
|
||||
dependencies:
|
||||
"@jest/types": "npm:30.3.0"
|
||||
"@types/node": "npm:*"
|
||||
chalk: "npm:^4.1.2"
|
||||
ci-info: "npm:^4.2.0"
|
||||
graceful-fs: "npm:^4.2.11"
|
||||
picomatch: "npm:^4.0.3"
|
||||
checksum: 10c0/eea6f39e52a8cb2b1a28bb315a90dc6a8e450fffed73bb5ef4489d02d86f7d91be600d83f1dcba22956b8ac5fefa8f1b250e636c8402d3e8b50a5eec8b5963b2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jest-worker@npm:^27.4.5":
|
||||
version: 27.5.1
|
||||
resolution: "jest-worker@npm:27.5.1"
|
||||
@@ -8081,7 +8341,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"js-tokens@npm:^3.0.0 || ^4.0.0":
|
||||
"js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "js-tokens@npm:4.0.0"
|
||||
checksum: 10c0/e248708d377aa058eacf2037b07ded847790e6de892bbad3dac0abba2e759cb9f121b00099a65195616badcb6eca8d14d975cb3e89eb1cfda644756402c8aeed
|
||||
@@ -9654,6 +9914,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-addon-api@npm:^8.3.0":
|
||||
version: 8.6.0
|
||||
resolution: "node-addon-api@npm:8.6.0"
|
||||
dependencies:
|
||||
node-gyp: "npm:latest"
|
||||
checksum: 10c0/869fe4fd13aef4feed3e4ca042136fd677675c061b13cde3b720dcd8e60439efe2538fbab841ed273ab8d5b077e1a0af66011141796589a5db0b5e6b183e2191
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-fetch@npm:^2.6.1":
|
||||
version: 2.6.8
|
||||
resolution: "node-fetch@npm:2.6.8"
|
||||
@@ -9693,7 +9962,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-gyp@npm:^11.4.2, node-gyp@npm:latest":
|
||||
"node-gyp@npm:^11.1.0, node-gyp@npm:^11.4.2, node-gyp@npm:latest":
|
||||
version: 11.5.0
|
||||
resolution: "node-gyp@npm:11.5.0"
|
||||
dependencies:
|
||||
@@ -10614,6 +10883,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pretty-format@npm:30.3.0, pretty-format@npm:^30.0.0":
|
||||
version: 30.3.0
|
||||
resolution: "pretty-format@npm:30.3.0"
|
||||
dependencies:
|
||||
"@jest/schemas": "npm:30.0.5"
|
||||
ansi-styles: "npm:^5.2.0"
|
||||
react-is: "npm:^18.3.1"
|
||||
checksum: 10c0/719b27d70cd8b01013485054c5d094e1fe85e093b09ee73553e3b19302da3cf54fbd6a7ea9577d6471aeff8d372200e56979ffc4c831e2133520bd18060895fb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pretty-ms@npm:^9.1.0":
|
||||
version: 9.1.0
|
||||
resolution: "pretty-ms@npm:9.1.0"
|
||||
@@ -10836,6 +11116,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-is@npm:^18.3.1":
|
||||
version: 18.3.1
|
||||
resolution: "react-is@npm:18.3.1"
|
||||
checksum: 10c0/f2f1e60010c683479e74c63f96b09fb41603527cd131a9959e2aee1e5a8b0caf270b365e5ca77d4a6b18aae659b60a86150bb3979073528877029b35aecd2072
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"read-pkg-up@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "read-pkg-up@npm:2.0.0"
|
||||
@@ -11738,6 +12025,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"slash@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "slash@npm:3.0.0"
|
||||
checksum: 10c0/e18488c6a42bdfd4ac5be85b2ced3ccd0224773baae6ad42cfbb9ec74fc07f9fa8396bd35ee638084ead7a2a0818eb5e7151111544d4731ce843019dab4be47b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"slash@npm:^5.1.0":
|
||||
version: 5.1.0
|
||||
resolution: "slash@npm:5.1.0"
|
||||
@@ -11931,6 +12225,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"stack-utils@npm:^2.0.6":
|
||||
version: 2.0.6
|
||||
resolution: "stack-utils@npm:2.0.6"
|
||||
dependencies:
|
||||
escape-string-regexp: "npm:^2.0.0"
|
||||
checksum: 10c0/651c9f87667e077584bbe848acaecc6049bc71979f1e9a46c7b920cad4431c388df0f51b8ad7cfd6eed3db97a2878d0fc8b3122979439ea8bac29c61c95eec8a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"standard-engine@npm:^15.0.0":
|
||||
version: 15.0.0
|
||||
resolution: "standard-engine@npm:15.0.0"
|
||||
|
||||
Reference in New Issue
Block a user