Compare commits

..

1 Commits

Author SHA1 Message Date
Keeley Hammond
ef8ec11bc1 chore: add skia patch dir 2026-03-09 12:34:51 -07:00
87 changed files with 472 additions and 1398 deletions

View File

@@ -43,7 +43,7 @@ runs:
curl --unix-socket /var/run/sas/sas.sock --fail "http://foo/$CACHE_FILE?platform=${{ inputs.target-platform }}&getAccountName=true" > sas-token
- name: Save SAS Key
if: ${{ inputs.generate-sas-token == 'true' }}
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: sas-token
key: sas-key-${{ inputs.target-platform }}-${{ github.run_number }}-${{ github.run_attempt }}

View File

@@ -7,7 +7,7 @@ runs:
shell: bash
id: yarn-cache-dir-path
run: echo "dir=$(node src/electron/script/yarn.js config get cacheFolder)" >> $GITHUB_OUTPUT
- uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}

View File

@@ -71,11 +71,3 @@ jobs:
uses: ./src/electron/.github/actions/checkout
with:
target-platform: linux
- name: Upload Patch Conflict Fix
if: ${{ failure() }}
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: update-patches
path: patches/update-patches.patch
if-no-files-found: ignore
archive: false

View File

@@ -209,7 +209,6 @@ jobs:
- name: Run Electron Tests
shell: bash
timeout-minutes: 40
env:
MOCHA_REPORTER: mocha-multi-reporters
MOCHA_MULTI_REPORTERS: mocha-junit-reporter, tap
@@ -260,19 +259,6 @@ jobs:
fi
fi
- name: Take screenshot on timeout or cancellation
if: ${{ inputs.target-platform != 'linux' && (cancelled() || failure()) }}
shell: bash
run: |
screenshot_dir="src/electron/spec/artifacts"
mkdir -p "$screenshot_dir"
screenshot_file="$screenshot_dir/screenshot-timeout-$(date +%Y%m%d%H%M%S).png"
if [ "${{ inputs.target-platform }}" = "macos" ]; then
screencapture -x "$screenshot_file" || true
elif [ "${{ inputs.target-platform }}" = "win" ]; then
powershell -command "Add-Type -AssemblyName System.Windows.Forms; \$screen = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds; \$bitmap = New-Object System.Drawing.Bitmap(\$screen.Width, \$screen.Height); \$graphics = [System.Drawing.Graphics]::FromImage(\$bitmap); \$graphics.CopyFromScreen(\$screen.Location, [System.Drawing.Point]::Empty, \$screen.Size); \$bitmap.Save('$screenshot_file')" || true
fi
- name: Upload Test results to Datadog
env:
DD_ENV: ci
@@ -288,7 +274,7 @@ jobs:
fi
if: always() && !cancelled()
- name: Upload Test Artifacts
if: always()
if: always() && !cancelled()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
with:
name: test_artifacts_${{ env.ARTIFACT_KEY }}_${{ matrix.shard }}

View File

@@ -9,8 +9,4 @@ npmMinimalAgeGate: 10080
npmPreapprovedPackages:
- "@electron/*"
httpProxy: "${HTTP_PROXY:-}"
httpsProxy: "${HTTPS_PROXY:-}"
yarnPath: .yarn/releases/yarn-4.12.0.cjs

View File

@@ -127,22 +127,6 @@ patches/{target}/*.patch → [e sync --3] → target repo commits
2. Create a git commit
3. Run `e patches <target>` to export
**Fixing patch conflicts on an existing PR:**
If asked to fix a patch conflict on a branch that already has an open PR, check the PR's failed **Apply Patches** CI run for an `update-patches` artifact before running `e sync` locally. CI has already performed the 3-way merge and exported the resolved patch diff — applying it is much faster than a full local sync.
```bash
# Find the failed Apply Patches run for the PR and download the artifact
gh run list --repo electron/electron --branch <pr-branch> --workflow "Apply Patches" --limit 1
gh run download <run-id> --repo electron/electron --name update-patches
# Apply the CI-generated fix, then push
git am update-patches.patch
git push
```
If no artifact exists (e.g. the 3-way merge itself failed), fall back to `e sync --3` and resolve manually.
## Testing
**Test location:** `spec/` directory

2
DEPS
View File

@@ -2,7 +2,7 @@ gclient_gn_args_from = 'src'
vars = {
'chromium_version':
'146.0.7680.153',
'146.0.7680.65',
'node_version':
'v24.14.0',
'nan_version':

View File

@@ -245,10 +245,6 @@ static_library("chrome") {
"//chrome/browser/ui/views/dark_mode_manager_linux.cc",
"//chrome/browser/ui/views/dark_mode_manager_linux.h",
]
sources += [
"//chrome/browser/ui/views/frame/browser_frame_view_paint_utils_linux.cc",
"//chrome/browser/ui/views/frame/browser_frame_view_paint_utils_linux.h",
]
public_deps += [ "//components/dbus" ]
}

View File

@@ -256,7 +256,7 @@ async function startRepl () {
if (option.file && !option.webdriver) {
const file = option.file;
// eslint-disable-next-line n/no-deprecated-api
const protocol = URL.canParse(file) ? new URL(file).protocol : null;
const protocol = url.parse(file).protocol;
const extension = path.extname(file);
if (protocol === 'http:' || protocol === 'https:' || protocol === 'file:' || protocol === 'chrome:') {
await loadApplicationByURL(file);

View File

@@ -1485,11 +1485,6 @@ mainWindow.webContents.setWindowOpenHandler((details) => {
const browserView = new BrowserView(options)
mainWindow.addBrowserView(browserView)
browserView.setBounds({ x: 0, y: 0, width: 640, height: 480 })
// For `background-tab` disposition (e.g., when middle-clicking or ctrl/cmd-clicking a link),
// `options.webContents` is undefined because its creation can be deferred. So load the URL manually.
if (details.disposition === 'background-tab') {
browserView.webContents.loadURL(details.url)
}
return browserView.webContents
}
}
@@ -2240,16 +2235,6 @@ Returns `string` - The identifier of a WebContents stream. This identifier can b
with `navigator.mediaDevices.getUserMedia` using a `chromeMediaSource` of `tab`.
The identifier is restricted to the web contents that it is registered to and is only valid for 10 seconds.
#### `contents.getOrCreateDevToolsTargetId()`
Returns `string` - The Chrome DevTools Protocol
[TargetID](https://chromedevtools.github.io/devtools-protocol/tot/Target/#type-TargetID)
associated with this WebContents. This is the reverse of
[`webContents.fromDevToolsTargetId()`](#webcontentsfromdevtoolstargetidtargetid).
> [!NOTE]
> This method creates a new DevTools agent for this WebContents if one does not already exist.
#### `contents.getOSProcessId()`
Returns `Integer` - The operating system `pid` of the associated renderer

View File

@@ -21,33 +21,24 @@
### Step 1: Fork
Fork Electron's [GitHub repository](https://github.com/electron/electron).
Fork the project [on GitHub](https://github.com/electron/electron) and clone your fork
locally.
```sh
$ git clone git@github.com:username/electron.git
$ cd electron
$ git remote add upstream https://github.com/electron/electron.git
$ git fetch upstream
```
### Step 2: Build
We recommend using [`@electron/build-tools`](https://github.com/electron/build-tools) to build
Electron itself.
Build steps and dependencies differ slightly depending on your operating system.
See these detailed guides on building Electron locally:
```sh
# Install build-tools package globally:
npm install -g @electron/build-tools
# Run the init script where you want to clone the project and point it to your fork:
e init --fork my-org/electron --bootstrap testing
```
This will create a new `electron` folder in your working directory and initialize the project.
Once the build completes, navigate to `electron/src/electron`, where your fork is actually cloned.
> [!IMPORTANT]
> Your Electron project has a complex folder structure with nested repositories.
> See the [Build Instructions](./build-instructions-gn.md) docs for detailed Build Tools
> usage instructions (e.g. how to sync dependencies or how to recompile the binary)
> and platform-specific notices.
There, you should have two `remote` URLs in git:
* `origin` will point to `electron/electron`
* `fork` will point to your fork (`my-org/electron`)
* [Building on macOS](build-instructions-macos.md)
* [Building on Linux](build-instructions-linux.md)
* [Building on Windows](build-instructions-windows.md)
Once you've built the project locally, you're ready to start making changes!
@@ -57,7 +48,7 @@ To keep your development environment organized, create local branches to
hold your work. These should be branched directly off of the `main` branch.
```sh
git checkout -b my-branch
$ git checkout -b my-branch -t upstream/main
```
## Making Changes
@@ -69,7 +60,7 @@ changes to either the C/C++ code in the `shell/` folder,
the JavaScript code in the `lib/` folder, the documentation in `docs/api/`
or tests in the `spec/` folder.
Please be sure to run `yarn lint` from time to time on any code changes
Please be sure to run `npm run lint` from time to time on any code changes
to ensure that they follow the project's code style.
See [coding style](coding-style.md) for
@@ -84,8 +75,8 @@ across multiple commits. There is no limit to the number of commits in a
pull request.
```sh
git add my/changed/files
git commit
$ git add my/changed/files
$ git commit
```
Note that multiple commits get squashed when they are landed.
@@ -147,8 +138,8 @@ Once you have committed your changes, it is a good idea to use `git rebase`
(not `git merge`) to synchronize your work with the main repository.
```sh
git fetch origin
git rebase origin/main
$ git fetch upstream
$ git rebase upstream/main
```
This ensures that your working branch has the latest changes from `electron/electron`
@@ -165,7 +156,7 @@ Before submitting your changes in a pull request, always run the full
test suite. To run the tests:
```sh
yarn test
$ npm run test
```
Make sure the linter does not report any issues and that all tests pass.
@@ -174,7 +165,7 @@ Please do not submit patches that fail either check.
If you are updating tests and want to run a single spec to check it:
```sh
yarn test -match=menu
$ npm run test -match=menu
```
The above would only run spec modules matching `menu`, which is useful for
@@ -188,7 +179,7 @@ begin the process of opening a pull request by pushing your working branch
to your fork on GitHub.
```sh
git push fork my-branch
$ git push origin my-branch
```
### Step 9: Opening the Pull Request
@@ -212,9 +203,9 @@ branch, add a new commit with those changes, and push those to your fork.
GitHub will automatically update the pull request.
```sh
git add my/changed/files
git commit
git push fork my-branch
$ git add my/changed/files
$ git commit
$ git push origin my-branch
```
There are a number of more advanced mechanisms for managing commits using
@@ -222,8 +213,8 @@ There are a number of more advanced mechanisms for managing commits using
Feel free to post a comment in the pull request to ping reviewers if you are
awaiting an answer on something. If you encounter words or acronyms that
seem unfamiliar, refer to the
[Chromium glossary](https://sites.google.com/a/chromium.org/dev/glossary).
seem unfamiliar, refer to this
[glossary](https://sites.google.com/a/chromium.org/dev/glossary).
#### Approval and Request Changes Workflow

View File

@@ -12,10 +12,6 @@ To create a frameless window, set the [`BaseWindowContructorOptions`][] `frame`
```
On Wayland (Linux), frameless windows have GTK drop shadows and extended
resize boundaries by default. To create a fully frameless window with no
decorations, set `hasShadow: false` in the window constructor options.
## Transparent windows
![Transparent Window](../images/transparent-window.png)

View File

@@ -146,15 +146,13 @@ The extra privileges granted to the `file://` protocol by this fuse are incomple
The `wasmTrapHandlers` fuse controls whether V8 will use signal handlers to trap Out of Bounds memory
access from WebAssembly. The feature works by surrounding the WebAssembly memory with large guard regions
and then installing a signal handler that traps attempt to access memory in the guard region. The feature
is only supported on the following 64-bit systems:
is only supported on the following 64-bit systems.
* Linux, macOS, Windows - x86_64
* Linux, macOS - aarch64
Linux. MacOS, Windows - x86_64
Linux, MacOS - aarch64
```text
| Guard Pages | WASM heap | Guard Pages |
|-----8GB-----| |-----8GB-----|
```
When the fuse is disabled V8 will use explicit bound checks in the generated WebAssembly code to ensure
memory safety. However, this method has some downsides

View File

@@ -50,7 +50,7 @@ sections.
In the main process, set an IPC listener on the `set-title` channel with the `ipcMain.on` API:
```js {7-11,23} title='main.js (Main Process)'
```js {6-10,22} title='main.js (Main Process)'
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('node:path')

View File

@@ -782,7 +782,8 @@ WebContents.prototype._init = function () {
const originCounts = new Map<string, number>();
const openDialogs = new Set<AbortController>();
this.on('-run-dialog', async (info, callback) => {
const origin = info.frame.origin === 'file://' ? info.frame.url : info.frame.origin;
const originUrl = new URL(info.frame.url);
const origin = originUrl.protocol === 'file:' ? originUrl.href : originUrl.origin;
if ((originCounts.get(origin) ?? 0) < 0) return callback(false, '');
const prefs = this.getLastWebPreferences();

View File

@@ -227,9 +227,10 @@ function validateHeader (name: any, value: any): void {
}
function parseOptions (optionsIn: ClientRequestConstructorOptions | string): NodeJS.CreateURLLoaderOptions & ExtraURLLoaderOptions {
const options: any = typeof optionsIn === 'string' ? new URL(optionsIn) : { ...optionsIn };
// eslint-disable-next-line n/no-deprecated-api
const options: any = typeof optionsIn === 'string' ? url.parse(optionsIn) : { ...optionsIn };
let urlStr: string = options.url || options.href;
let urlStr: string = options.url;
if (!urlStr) {
const urlObj: url.UrlObject = {};
@@ -259,8 +260,8 @@ function parseOptions (optionsIn: ClientRequestConstructorOptions | string): Nod
// an invalid request.
throw new TypeError('Request path contains unescaped characters');
}
const pathObj = new URL(options.path || '/', 'http://localhost');
// eslint-disable-next-line n/no-deprecated-api
const pathObj = url.parse(options.path || '/');
urlObj.pathname = pathObj.pathname;
urlObj.search = pathObj.search;
urlObj.hash = pathObj.hash;

View File

@@ -1232,8 +1232,6 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
// has filesystem caching.
overrideAPI(fs, 'copyFile');
overrideAPISync(fs, 'copyFileSync');
overrideAPI(fs, 'cp');
overrideAPISync(fs, 'cpSync');
overrideAPI(fs, 'open');
overrideAPISync(process, 'dlopen', 1);

View File

@@ -146,4 +146,3 @@ fix_os_crypt_async_cookie_encryption.patch
fix_update_dbus_signal_signature_for_xdg_globalshortcuts_portal.patch
fix_set_correct_app_id_on_linux.patch
fix_pass_trigger_for_global_shortcuts_on_wayland.patch
feat_plumb_node_integration_in_worker_through_workersettings.patch

View File

@@ -33,10 +33,10 @@ index 4a742db71f62f9ac891ceeb0604ca0b99d1d89c1..2c5af6482e2b6905552a05b16d3df0a4
"//base",
"//build:branding_buildflags",
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index a2a14349d40ce34831ab063cd5eb55cd5085c814..1a861ff7867f19935178c8368a9a720230fee026 100644
index 2fc3a991d89093ff9139eb09d74123197155caff..0862aa96c2a7b496338ac0593f84fcfa21f25572 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -4751,7 +4751,7 @@ static_library("browser") {
@@ -4749,7 +4749,7 @@ static_library("browser") {
]
}
@@ -46,10 +46,10 @@ index a2a14349d40ce34831ab063cd5eb55cd5085c814..1a861ff7867f19935178c8368a9a7202
# than here in :chrome_dll.
deps += [ "//chrome:packed_resources_integrity_header" ]
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index 40ea51f97470e2b86f8d2d373ea99a2a71ad185e..db6a2291ce77d89c8e28a1435336fd939e436906 100644
index 7d5a246787bc3cc3bcb883aa78121d3d3f124780..b5de35620bc636d5e1d0d5770d898f564843bcef 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -7731,9 +7731,12 @@ test("unit_tests") {
@@ -7728,9 +7728,12 @@ test("unit_tests") {
"//chrome/notification_helper",
]
@@ -63,7 +63,7 @@ index 40ea51f97470e2b86f8d2d373ea99a2a71ad185e..db6a2291ce77d89c8e28a1435336fd93
"//chrome//services/util_win:unit_tests",
"//chrome/app:chrome_dll_resources",
"//chrome/app:win_unit_tests",
@@ -8703,6 +8706,10 @@ test("unit_tests") {
@@ -8698,6 +8701,10 @@ test("unit_tests") {
"../browser/performance_manager/policies/background_tab_loading_policy_unittest.cc",
]
@@ -74,7 +74,7 @@ index 40ea51f97470e2b86f8d2d373ea99a2a71ad185e..db6a2291ce77d89c8e28a1435336fd93
sources += [
# The importer code is not used on Android.
"../common/importer/firefox_importer_utils_unittest.cc",
@@ -8760,7 +8767,6 @@ test("unit_tests") {
@@ -8755,7 +8762,6 @@ test("unit_tests") {
# TODO(crbug.com/417513088): Maybe merge with the non-android `deps` declaration above?
deps += [
"../browser/screen_ai:screen_ai_install_state",

View File

@@ -9,10 +9,10 @@ potentially prevent a window from being created.
TODO(loc): this patch is currently broken.
diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index 2d8a70f5fc0f6c2dc2a7587b7bc2e43dbcee8f0e..a87bd09d7a12c5f003488792843cd1807ee1e30f 100644
index 46368e70af175d8d0ab0fb5a36d258e48270371e..8d7be769a6c76650ae999338578215dcd324c199 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -9997,6 +9997,7 @@ void RenderFrameHostImpl::CreateNewWindow(
@@ -9990,6 +9990,7 @@ void RenderFrameHostImpl::CreateNewWindow(
last_committed_origin_, params->window_container_type,
params->target_url, params->referrer.To<Referrer>(),
params->frame_name, params->disposition, *params->features,

View File

@@ -65,7 +65,7 @@ index 2748dd196fe1f56357348a204e24f0b8a28b97dd..5800dd00b47c657d9e6766f3fc5a3065
#if BUILDFLAG(IS_WIN)
bool EscapeVirtualization(const base::FilePath& user_data_dir);
diff --git a/chrome/browser/process_singleton_posix.cc b/chrome/browser/process_singleton_posix.cc
index 73aa4cb9652870b0bff4684d7c72ae7dbd852db8..144788ceadea85c9d1fae12d1ba4dbc1fc7cd699 100644
index 73aa4cb9652870b0bff4684d7c72ae7dbd852db8..b55c942a8ccb326e4898172a7b4f6c0aa3183a0b 100644
--- a/chrome/browser/process_singleton_posix.cc
+++ b/chrome/browser/process_singleton_posix.cc
@@ -619,6 +619,7 @@ class ProcessSingleton::LinuxWatcher
@@ -106,41 +106,22 @@ index 73aa4cb9652870b0bff4684d7c72ae7dbd852db8..144788ceadea85c9d1fae12d1ba4dbc1
const size_t kMinMessageLength = kStartToken.length() + 4;
if (bytes_read_ < kMinMessageLength) {
buf_[bytes_read_] = 0;
@@ -745,10 +751,45 @@ void ProcessSingleton::LinuxWatcher::SocketReader::
@@ -745,10 +751,26 @@ void ProcessSingleton::LinuxWatcher::SocketReader::
tokens.erase(tokens.begin());
tokens.erase(tokens.begin());
+ size_t num_args;
+ if (!base::StringToSizeT(tokens[0], &num_args) ||
+ num_args > tokens.size() - 1) {
+ LOG(ERROR) << "Invalid num_args in socket message";
+ CleanupAndDeleteSelf();
+ return;
+ }
+ std::vector<std::string> command_line(tokens.begin() + 1,
+ tokens.begin() + 1 + num_args);
+ base::StringToSizeT(tokens[0], &num_args);
+ std::vector<std::string> command_line(tokens.begin() + 1, tokens.begin() + 1 + num_args);
+
+ std::vector<uint8_t> additional_data;
+ // After consuming [num_args, argv...], two more tokens are needed for
+ // additional data: [size, payload]. Subtract to avoid overflow when
+ // num_args is large.
+ if (tokens.size() - 1 - num_args >= 2) {
+ if (tokens.size() >= 3 + num_args) {
+ size_t additional_data_size;
+ if (!base::StringToSizeT(tokens[1 + num_args], &additional_data_size)) {
+ LOG(ERROR) << "Invalid additional_data_size in socket message";
+ CleanupAndDeleteSelf();
+ return;
+ }
+ base::StringToSizeT(tokens[1 + num_args], &additional_data_size);
+ std::string remaining_args = base::JoinString(
+ base::span(tokens).subspan(2 + num_args),
+ std::string(1, kTokenDelimiter));
+ if (additional_data_size > remaining_args.size()) {
+ LOG(ERROR) << "additional_data_size exceeds payload length";
+ CleanupAndDeleteSelf();
+ return;
+ }
+ const auto adspan =
+ base::as_byte_span(remaining_args).first(additional_data_size);
+ const auto adspan = base::as_byte_span(remaining_args).first(additional_data_size);
+ additional_data.assign(adspan.begin(), adspan.end());
+ }
+
@@ -153,7 +134,7 @@ index 73aa4cb9652870b0bff4684d7c72ae7dbd852db8..144788ceadea85c9d1fae12d1ba4dbc1
fd_watch_controller_.reset();
// LinuxWatcher::HandleMessage() is in charge of destroying this SocketReader
@@ -777,8 +818,10 @@ void ProcessSingleton::LinuxWatcher::SocketReader::FinishWithACK(
@@ -777,8 +799,10 @@ void ProcessSingleton::LinuxWatcher::SocketReader::FinishWithACK(
//
ProcessSingleton::ProcessSingleton(
const base::FilePath& user_data_dir,
@@ -164,7 +145,7 @@ index 73aa4cb9652870b0bff4684d7c72ae7dbd852db8..144788ceadea85c9d1fae12d1ba4dbc1
current_pid_(base::GetCurrentProcId()) {
socket_path_ = user_data_dir.Append(chrome::kSingletonSocketFilename);
lock_path_ = user_data_dir.Append(chrome::kSingletonLockFilename);
@@ -899,7 +942,8 @@ ProcessSingleton::NotifyResult ProcessSingleton::NotifyOtherProcessWithTimeout(
@@ -899,7 +923,8 @@ ProcessSingleton::NotifyResult ProcessSingleton::NotifyOtherProcessWithTimeout(
sizeof(socket_timeout));
// Found another process, prepare our command line
@@ -174,7 +155,7 @@ index 73aa4cb9652870b0bff4684d7c72ae7dbd852db8..144788ceadea85c9d1fae12d1ba4dbc1
std::string to_send(kStartToken);
to_send.push_back(kTokenDelimiter);
@@ -909,11 +953,21 @@ ProcessSingleton::NotifyResult ProcessSingleton::NotifyOtherProcessWithTimeout(
@@ -909,11 +934,21 @@ ProcessSingleton::NotifyResult ProcessSingleton::NotifyOtherProcessWithTimeout(
to_send.append(current_dir.value());
const std::vector<std::string>& argv = cmd_line.argv();
@@ -197,18 +178,10 @@ index 73aa4cb9652870b0bff4684d7c72ae7dbd852db8..144788ceadea85c9d1fae12d1ba4dbc1
if (!WriteToSocket(socket.fd(), to_send)) {
// Try to kill the other process, because it might have been dead.
diff --git a/chrome/browser/process_singleton_win.cc b/chrome/browser/process_singleton_win.cc
index ae659d84a5ae2f2e87ce288477506575f8d86839..274887d62ff8d008bb86815a11205fcaa5f2c2ff 100644
index ae659d84a5ae2f2e87ce288477506575f8d86839..d93c7e8487ab1a2bbb5f56f2ca44868f947e6bfc 100644
--- a/chrome/browser/process_singleton_win.cc
+++ b/chrome/browser/process_singleton_win.cc
@@ -9,6 +9,7 @@
#include <shellapi.h>
#include <stddef.h>
+#include "base/base64.h"
#include "base/base_paths.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
@@ -81,10 +82,12 @@ BOOL CALLBACK BrowserWindowEnumeration(HWND window, LPARAM param) {
@@ -81,10 +81,12 @@ BOOL CALLBACK BrowserWindowEnumeration(HWND window, LPARAM param) {
bool ParseCommandLine(const COPYDATASTRUCT* cds,
base::CommandLine* parsed_command_line,
@@ -223,7 +196,7 @@ index ae659d84a5ae2f2e87ce288477506575f8d86839..274887d62ff8d008bb86815a11205fca
static const int min_message_size = 7;
if (cds->cbData < min_message_size * sizeof(wchar_t) ||
cds->cbData % sizeof(wchar_t) != 0) {
@@ -134,6 +137,25 @@ bool ParseCommandLine(const COPYDATASTRUCT* cds,
@@ -134,6 +136,23 @@ bool ParseCommandLine(const COPYDATASTRUCT* cds,
const std::wstring cmd_line =
msg.substr(second_null + 1, third_null - second_null);
*parsed_command_line = base::CommandLine::FromString(cmd_line);
@@ -236,20 +209,18 @@ index ae659d84a5ae2f2e87ce288477506575f8d86839..274887d62ff8d008bb86815a11205fca
+ return true;
+ }
+
+ // Get the actual additional data. It is base64-encoded so it can
+ // safely traverse the null-delimited wchar_t buffer.
+ const std::wstring encoded_w =
+ msg.substr(third_null + 1, fourth_null - third_null - 1);
+ std::string encoded = base::WideToASCII(encoded_w);
+ std::optional<std::vector<uint8_t>> decoded = base::Base64Decode(encoded);
+ if (decoded) {
+ *parsed_additional_data = std::move(*decoded);
+ }
+ // Get the actual additional data.
+ const std::wstring additional_data =
+ msg.substr(third_null + 1, fourth_null - third_null);
+ base::span<const uint8_t> additional_data_bytes =
+ base::as_byte_span(additional_data);
+ *parsed_additional_data = std::vector<uint8_t>(
+ additional_data_bytes.begin(), additional_data_bytes.end());
+
return true;
}
return false;
@@ -155,13 +177,14 @@ bool ProcessLaunchNotification(
@@ -155,13 +174,14 @@ bool ProcessLaunchNotification(
base::CommandLine parsed_command_line(base::CommandLine::NO_PROGRAM);
base::FilePath current_directory;
@@ -267,7 +238,7 @@ index ae659d84a5ae2f2e87ce288477506575f8d86839..274887d62ff8d008bb86815a11205fca
return true;
}
@@ -265,9 +288,11 @@ bool ProcessSingleton::EscapeVirtualization(
@@ -265,9 +285,11 @@ bool ProcessSingleton::EscapeVirtualization(
ProcessSingleton::ProcessSingleton(
const std::string& program_name,
const base::FilePath& user_data_dir,
@@ -279,7 +250,7 @@ index ae659d84a5ae2f2e87ce288477506575f8d86839..274887d62ff8d008bb86815a11205fca
program_name_(program_name),
is_app_sandboxed_(is_app_sandboxed),
is_virtualized_(false),
@@ -294,7 +319,7 @@ ProcessSingleton::NotifyResult ProcessSingleton::NotifyOtherProcess() {
@@ -294,7 +316,7 @@ ProcessSingleton::NotifyResult ProcessSingleton::NotifyOtherProcess() {
return PROCESS_NONE;
}
@@ -289,18 +260,10 @@ index ae659d84a5ae2f2e87ce288477506575f8d86839..274887d62ff8d008bb86815a11205fca
return PROCESS_NOTIFIED;
case NotifyChromeResult::kFailed:
diff --git a/chrome/browser/win/chrome_process_finder.cc b/chrome/browser/win/chrome_process_finder.cc
index 594f3bc08a4385c177fb488123cef79448e94850..28e5a18a19718b2e748ada6882341413a1ab0705 100644
index 594f3bc08a4385c177fb488123cef79448e94850..5a1dde19a4bc2bf728eba4c738f831c3e5b73942 100644
--- a/chrome/browser/win/chrome_process_finder.cc
+++ b/chrome/browser/win/chrome_process_finder.cc
@@ -11,6 +11,7 @@
#include <string>
#include <string_view>
+#include "base/base64.h"
#include "base/check.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
@@ -39,7 +40,9 @@ HWND FindRunningChromeWindow(const base::FilePath& user_data_dir) {
@@ -39,7 +39,9 @@ HWND FindRunningChromeWindow(const base::FilePath& user_data_dir) {
return base::win::MessageWindow::FindWindow(user_data_dir.value());
}
@@ -311,7 +274,7 @@ index 594f3bc08a4385c177fb488123cef79448e94850..28e5a18a19718b2e748ada6882341413
TRACE_EVENT0("startup", "AttemptToNotifyRunningChrome");
DCHECK(remote_window);
@@ -70,12 +73,22 @@ NotifyChromeResult AttemptToNotifyRunningChrome(HWND remote_window) {
@@ -70,12 +72,24 @@ NotifyChromeResult AttemptToNotifyRunningChrome(HWND remote_window) {
new_command_line.AppendSwitch(switches::kSourceAppId);
}
// Send the command line to the remote chrome window.
@@ -323,12 +286,14 @@ index 594f3bc08a4385c177fb488123cef79448e94850..28e5a18a19718b2e748ada6882341413
std::wstring_view{L"\0", 1}, new_command_line.GetCommandLineString(),
std::wstring_view{L"\0", 1}});
+ if (!additional_data.empty()) {
+ // Base64-encode so the payload survives the null-delimited wchar_t
+ // framing; raw serialized bytes can contain 0x0000 sequences which
+ // would otherwise terminate the field early.
+ std::string encoded = base::Base64Encode(additional_data);
+ to_send.append(base::ASCIIToWide(encoded));
+ size_t additional_data_size = additional_data.size_bytes();
+ if (additional_data_size) {
+ size_t padded_size = additional_data_size / sizeof(wchar_t);
+ if (additional_data_size % sizeof(wchar_t) != 0) {
+ padded_size++;
+ }
+ to_send.append(reinterpret_cast<const wchar_t*>(additional_data.data()),
+ padded_size);
+ to_send.append(L"\0", 1); // Null separator.
+ }
+

View File

@@ -1,73 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Samuel Attard <sattard@anthropic.com>
Date: Sat, 7 Mar 2026 23:07:30 -0800
Subject: feat: plumb node_integration_in_worker through WorkerSettings
Copy the node_integration_in_worker flag from the initiating frame's
WebPreferences into WorkerSettings at dedicated worker creation time,
so the value is readable per-worker on the worker thread rather than
relying on a process-wide command line switch. The value is also
propagated to nested workers via WorkerSettings::Copy.
diff --git a/third_party/blink/renderer/core/workers/dedicated_worker.cc b/third_party/blink/renderer/core/workers/dedicated_worker.cc
index 8a558e1a1f84c3dbed07143680c9c088084051b4..8895e76170cd7e79a93477cd826dcd84fa102d81 100644
--- a/third_party/blink/renderer/core/workers/dedicated_worker.cc
+++ b/third_party/blink/renderer/core/workers/dedicated_worker.cc
@@ -37,6 +37,7 @@
#include "third_party/blink/renderer/core/frame/local_frame_client.h"
#include "third_party/blink/renderer/core/frame/web_frame_widget_impl.h"
#include "third_party/blink/renderer/core/frame/web_local_frame_impl.h"
+#include "third_party/blink/renderer/core/exported/web_view_impl.h"
#include "third_party/blink/renderer/core/inspector/inspector_trace_events.h"
#include "third_party/blink/renderer/core/inspector/main_thread_debugger.h"
#include "third_party/blink/renderer/core/loader/document_loader.h"
@@ -557,6 +558,12 @@ DedicatedWorker::CreateGlobalScopeCreationParams(
auto* frame = window->GetFrame();
parent_devtools_token = frame->GetDevToolsFrameToken();
settings = std::make_unique<WorkerSettings>(frame->GetSettings());
+ if (auto* web_local_frame = WebLocalFrameImpl::FromFrame(frame)) {
+ if (auto* web_view = web_local_frame->ViewImpl()) {
+ settings->SetNodeIntegrationInWorker(
+ web_view->GetWebPreferences().node_integration_in_worker);
+ }
+ }
agent_group_scheduler_compositor_task_runner =
execution_context->GetScheduler()
->ToFrameScheduler()
diff --git a/third_party/blink/renderer/core/workers/worker_settings.cc b/third_party/blink/renderer/core/workers/worker_settings.cc
index 45680c5f6ea0c7e89ccf43eb88f8a11e3318c02e..3fa3af62f4e7ba8186441c5e3184b1c04fe32d12 100644
--- a/third_party/blink/renderer/core/workers/worker_settings.cc
+++ b/third_party/blink/renderer/core/workers/worker_settings.cc
@@ -40,6 +40,8 @@ std::unique_ptr<WorkerSettings> WorkerSettings::Copy(
old_settings->strictly_block_blockable_mixed_content_;
new_settings->generic_font_family_settings_ =
old_settings->generic_font_family_settings_;
+ new_settings->node_integration_in_worker_ =
+ old_settings->node_integration_in_worker_;
return new_settings;
}
diff --git a/third_party/blink/renderer/core/workers/worker_settings.h b/third_party/blink/renderer/core/workers/worker_settings.h
index 45c60dd2c44b05fdd279f759069383479823c7f2..33a2a0337efb9a46293e11d0d09b3fc182ab9618 100644
--- a/third_party/blink/renderer/core/workers/worker_settings.h
+++ b/third_party/blink/renderer/core/workers/worker_settings.h
@@ -43,6 +43,11 @@ class CORE_EXPORT WorkerSettings {
return generic_font_family_settings_;
}
+ bool NodeIntegrationInWorker() const { return node_integration_in_worker_; }
+ void SetNodeIntegrationInWorker(bool value) {
+ node_integration_in_worker_ = value;
+ }
+
private:
void CopyFlagValuesFromSettings(Settings*);
@@ -54,6 +59,7 @@ class CORE_EXPORT WorkerSettings {
bool strict_mixed_content_checking_ = false;
bool allow_running_of_insecure_content_ = false;
bool strictly_block_blockable_mixed_content_ = false;
+ bool node_integration_in_worker_ = false;
GenericFontFamilySettings generic_font_family_settings_;
};

View File

@@ -28,7 +28,7 @@ The patch should be removed in favor of either:
Upstream bug https://bugs.chromium.org/p/chromium/issues/detail?id=1081397.
diff --git a/content/browser/renderer_host/navigation_request.cc b/content/browser/renderer_host/navigation_request.cc
index 7d101d40116bf743f940f32ba4c9b507aa9a235b..2aa1584fd451fb15ec6084fb0c19724e6c63e0e3 100644
index 5b79df01e0a5ee81919ebed7d689e430fe7fe305..b11808a69483f4cbcc56d90cc6161984df90c1e4 100644
--- a/content/browser/renderer_host/navigation_request.cc
+++ b/content/browser/renderer_host/navigation_request.cc
@@ -11666,6 +11666,11 @@ url::Origin NavigationRequest::GetOriginForURLLoaderFactoryUnchecked() {

View File

@@ -17,7 +17,7 @@ Revert "Reland "Port net::CookieCryptoDelegate to os_crypt async""
This reverts commit f01b115c7e21a09cc762f65bf7fd9c6ea9d9d0f8.
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index 1a861ff7867f19935178c8368a9a720230fee026..b1ca947122f4ea715be18a0fd4e75b30fffc5a3c 100644
index 0862aa96c2a7b496338ac0593f84fcfa21f25572..aed5a316bd3d97df715f779273ae4c283cd29c92 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -714,6 +714,8 @@ static_library("browser") {

View File

@@ -1189,7 +1189,7 @@ index a1068589ad844518038ee7bc15a3de9bc5cba525..1ff781c49f086ec8015c7d3c44567dbe
} // namespace content
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index d368b2481156bb79c6e74c8b09a828eb2fa2d44c..07cbf495717714d71d977a8820e08050c3062526 100644
index 8575983261c7b57fc85097edb94a8e6f306974f9..aae50b6830450baf27f2834a8187540d7ff6eb35 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -700,6 +700,7 @@ static_library("test_support") {
@@ -1217,7 +1217,7 @@ index d368b2481156bb79c6e74c8b09a828eb2fa2d44c..07cbf495717714d71d977a8820e08050
]
if (!(is_chromeos && target_cpu == "arm64" && current_cpu == "arm")) {
@@ -3412,6 +3416,7 @@ test("content_unittests") {
@@ -3411,6 +3415,7 @@ test("content_unittests") {
"//ui/shell_dialogs",
"//ui/webui:test_support",
"//url",

View File

@@ -10,10 +10,10 @@ on Windows. We should refactor our code so that this patch isn't
necessary.
diff --git a/testing/variations/fieldtrial_testing_config.json b/testing/variations/fieldtrial_testing_config.json
index b50c4004adfa883dfd670611f45856454517e877..a2086481f5120b36400588dfb2b941457e42ae67 100644
index eecdbe8a279ef1a7d9aed4f5496e871d54092e0f..16ff3ffbe8300534cef76f857284ef92ad0a88f6 100644
--- a/testing/variations/fieldtrial_testing_config.json
+++ b/testing/variations/fieldtrial_testing_config.json
@@ -27080,6 +27080,21 @@
@@ -27059,6 +27059,21 @@
]
}
],

View File

@@ -15,7 +15,7 @@ Note that we also need to manually update embedder's
`api::WebContents::IsFullscreenForTabOrPending` value.
diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index a87bd09d7a12c5f003488792843cd1807ee1e30f..b38240fd422163f09bfb8d4b40213a1940a72acd 100644
index 8d7be769a6c76650ae999338578215dcd324c199..3e8021289c00ec6b15457b17173dfed386eac2fe 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -9097,6 +9097,17 @@ void RenderFrameHostImpl::EnterFullscreen(

View File

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

View File

@@ -1,2 +1 @@
chore_expose_ui_to_allow_electron_to_set_dock_side.patch
fix_prefer_browser_runtime_over_node_in_hostruntime_detection.patch

View File

@@ -1,36 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Shelley Vohr <shelley.vohr@gmail.com>
Date: Thu, 12 Mar 2026 17:03:29 +0100
Subject: fix: prefer browser runtime over node in HostRuntime detection
In Electron, the `process` global is available in renderer processes,
including the DevTools renderer. This causes the IS_NODE check to pass,
leading DevTools to attempt importing the Node.js platform runtime
(which uses `node:worker_threads`). However, DevTools Web Workers
running under the `devtools://` protocol don't have access to Node.js
built-in modules, resulting in a failed dynamic import.
Fix by checking IS_BROWSER first, since DevTools always runs in a
browser-like environment. The Node.js runtime is only needed when
DevTools runs under pure Node.js (e.g., CLI tooling or testing).
diff --git a/front_end/core/platform/HostRuntime.ts b/front_end/core/platform/HostRuntime.ts
index 91adba7c966a9c4c0e5315d2cfee07f8f622b731..16822b8d4ea74a4ffd6870e5e95948d75918f5d2 100644
--- a/front_end/core/platform/HostRuntime.ts
+++ b/front_end/core/platform/HostRuntime.ts
@@ -14,12 +14,12 @@ export const IS_BROWSER =
typeof window !== 'undefined' || (typeof self !== 'undefined' && typeof self.postMessage === 'function');
export const HOST_RUNTIME = await (async(): Promise<Api.HostRuntime.HostRuntime> => {
- if (IS_NODE) {
- return (await import('./node/node.js')).HostRuntime.HOST_RUNTIME;
- }
if (IS_BROWSER) {
return (await import('./browser/browser.js')).HostRuntime.HOST_RUNTIME;
}
+ if (IS_NODE) {
+ return (await import('./node/node.js')).HostRuntime.HOST_RUNTIME;
+ }
throw new Error('Unknown runtime!');
})();

View File

@@ -17,7 +17,7 @@ Upstreams:
- https://github.com/nodejs/node/pull/39136
diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc
index 461819ce0fa732048e4365c40a86ef55d984c35f..f1c85e94cf526d0255f47c003664680d26413ec3 100644
index 461819ce0fa732048e4365c40a86ef55d984c35f..fa55c980a9c4f373723a867fd41276d67b0b9413 100644
--- a/deps/ncrypto/ncrypto.cc
+++ b/deps/ncrypto/ncrypto.cc
@@ -11,6 +11,7 @@
@@ -28,6 +28,38 @@ index 461819ce0fa732048e4365c40a86ef55d984c35f..f1c85e94cf526d0255f47c003664680d
#if OPENSSL_VERSION_MAJOR >= 3
#include <openssl/core_names.h>
#include <openssl/params.h>
@@ -1130,7 +1131,9 @@ int64_t X509View::getValidToTime() const {
return tp;
#else
struct tm tp;
- ASN1_TIME_to_tm(X509_get0_notAfter(cert_), &tp);
+#ifndef OPENSSL_IS_BORINGSSL
+ ASN1_TIME_to_tm(X509_get0_notAfter(cert_), &tp);
+#endif
return PortableTimeGM(&tp);
#endif
}
@@ -1142,7 +1145,9 @@ int64_t X509View::getValidFromTime() const {
return tp;
#else
struct tm tp;
+#ifndef OPENSSL_IS_BORINGSSL
ASN1_TIME_to_tm(X509_get0_notBefore(cert_), &tp);
+#endif
return PortableTimeGM(&tp);
#endif
}
@@ -2886,10 +2891,6 @@ std::optional<uint32_t> SSLPointer::verifyPeerCertificate() const {
const char* SSLPointer::getClientHelloAlpn() const {
if (ssl_ == nullptr) return {};
#ifndef OPENSSL_IS_BORINGSSL
- const unsigned char* buf;
- size_t len;
- size_t rem;
-
if (!SSL_client_hello_get0_ext(
get(),
TLSEXT_TYPE_application_layer_protocol_negotiation,
@@ -3090,9 +3091,11 @@ const Cipher Cipher::AES_256_GCM = Cipher::FromNid(NID_aes_256_gcm);
const Cipher Cipher::AES_128_KW = Cipher::FromNid(NID_id_aes128_wrap);
const Cipher Cipher::AES_192_KW = Cipher::FromNid(NID_id_aes192_wrap);

0
patches/skia/.patches Normal file
View File

View File

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

View File

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

View File

@@ -1,130 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Andy Locascio <loc@anthropic.com>
Date: Tue, 6 Jan 2026 08:23:03 -0800
Subject: fix: clean up orphaned staged updates before downloading new update
When checkForUpdates() is called while an update is already staged,
Squirrel creates a new temporary directory for the download without
cleaning up the old one. This can lead to significant disk usage if
the app keeps checking for updates without restarting.
This change adds a pruneOrphanedUpdateDirectories step before creating
a new temp directory. Unlike a blanket prune, this reads the current
ShipItState.plist and preserves the directory it references, deleting
only truly orphaned update directories. This keeps the on-disk
footprint bounded (at most 2 dirs) while ensuring quitAndInstall
remains safe to call even when a new check is in progress.
Refs https://github.com/electron/electron/issues/50200
diff --git a/Squirrel/SQRLUpdater.m b/Squirrel/SQRLUpdater.m
index d156616e81e6f25a3bded30e6216b8fc311f31bc..41856e5754228d33982db72f97f2ff241615a357 100644
--- a/Squirrel/SQRLUpdater.m
+++ b/Squirrel/SQRLUpdater.m
@@ -543,11 +543,19 @@ - (RACSignal *)downloadBundleForUpdate:(SQRLUpdate *)update intoDirectory:(NSURL
#pragma mark File Management
- (RACSignal *)uniqueTemporaryDirectoryForUpdate {
- return [[[RACSignal
+ // Clean up any orphaned update directories before creating a new one.
+ // This prevents disk usage from growing when checkForUpdates() is called
+ // multiple times without the app restarting. The currently staged update
+ // (referenced by ShipItState.plist) is always preserved so quitAndInstall
+ // remains safe to call while a new check is in progress.
+ return [[[[[self
+ pruneOrphanedUpdateDirectories]
+ ignoreValues]
+ concat:[RACSignal
defer:^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
return [directoryManager storageURL];
- }]
+ }]]
flattenMap:^(NSURL *storageURL) {
NSURL *updateDirectoryTemplate = [storageURL URLByAppendingPathComponent:[SQRLUpdaterUniqueTemporaryDirectoryPrefix stringByAppendingString:@"XXXXXXX"]];
char *updateDirectoryCString = strdup(updateDirectoryTemplate.path.fileSystemRepresentation);
@@ -668,25 +676,68 @@ - (RACSignal *)pruneUpdateDirectories {
return [directoryManager storageURL];
}]
flattenMap:^(NSURL *storageURL) {
- NSFileManager *manager = [[NSFileManager alloc] init];
- NSDirectoryEnumerator *enumerator = [manager enumeratorAtURL:storageURL includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsSubdirectoryDescendants errorHandler:^(NSURL *URL, NSError *error) {
- NSLog(@"Error enumerating item %@ within directory %@: %@", URL, storageURL, error);
- return YES;
- }];
+ return [self removeUpdateDirectoriesInStorageURL:storageURL excludingURL:nil];
+ }]
+ setNameWithFormat:@"%@ -prunedUpdateDirectories", self];
+}
- return [[enumerator.rac_sequence.signal
- filter:^(NSURL *enumeratedURL) {
- NSString *name = enumeratedURL.lastPathComponent;
- return [name hasPrefix:SQRLUpdaterUniqueTemporaryDirectoryPrefix];
- }]
- doNext:^(NSURL *directoryURL) {
- NSError *error = nil;
- if (![manager removeItemAtURL:directoryURL error:&error]) {
- NSLog(@"Error removing old update directory at %@: %@", directoryURL, error.sqrl_verboseDescription);
- }
+/// Lazily removes orphaned temporary directories upon subscription, always
+/// preserving the directory currently referenced by ShipItState.plist so that
+/// quitAndInstall remains safe to call mid-check.
+///
+/// Safe to call in any state. Sends each removed directory then completes on
+/// an unspecified thread. Errors reading the staged request are swallowed
+/// (treated as "nothing staged").
+- (RACSignal *)pruneOrphanedUpdateDirectories {
+ return [[[[[SQRLShipItRequest
+ readUsingURL:self.shipItStateURL]
+ map:^(SQRLShipItRequest *request) {
+ // The request holds the URL to the staged .app bundle; its parent
+ // is the update.XXXXXXX directory we must preserve.
+ return [request.updateBundleURL URLByDeletingLastPathComponent];
+ }]
+ catch:^(NSError *error) {
+ // No staged request (or unreadable) — nothing to preserve.
+ return [RACSignal return:nil];
+ }]
+ flattenMap:^(NSURL *stagedDirectoryURL) {
+ SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
+ return [[directoryManager storageURL]
+ flattenMap:^(NSURL *storageURL) {
+ return [self removeUpdateDirectoriesInStorageURL:storageURL excludingURL:stagedDirectoryURL];
}];
}]
- setNameWithFormat:@"%@ -prunedUpdateDirectories", self];
+ setNameWithFormat:@"%@ -pruneOrphanedUpdateDirectories", self];
+}
+
+/// Shared enumerate-and-delete logic for update temp directories.
+///
+/// storageURL - The Squirrel storage root to enumerate. Must not be nil.
+/// excludedURL - Directory to skip (compared by standardized path). May be nil.
+- (RACSignal *)removeUpdateDirectoriesInStorageURL:(NSURL *)storageURL excludingURL:(NSURL *)excludedURL {
+ NSParameterAssert(storageURL != nil);
+
+ NSFileManager *manager = [[NSFileManager alloc] init];
+ NSDirectoryEnumerator *enumerator = [manager enumeratorAtURL:storageURL includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsSubdirectoryDescendants errorHandler:^(NSURL *URL, NSError *error) {
+ NSLog(@"Error enumerating item %@ within directory %@: %@", URL, storageURL, error);
+ return YES;
+ }];
+
+ NSString *excludedPath = excludedURL.URLByStandardizingPath.path;
+
+ return [[enumerator.rac_sequence.signal
+ filter:^(NSURL *enumeratedURL) {
+ NSString *name = enumeratedURL.lastPathComponent;
+ if (![name hasPrefix:SQRLUpdaterUniqueTemporaryDirectoryPrefix]) return NO;
+ if (excludedPath != nil && [enumeratedURL.URLByStandardizingPath.path isEqualToString:excludedPath]) return NO;
+ return YES;
+ }]
+ doNext:^(NSURL *directoryURL) {
+ NSError *error = nil;
+ if (![manager removeItemAtURL:directoryURL error:&error]) {
+ NSLog(@"Error removing old update directory at %@: %@", directoryURL, error.sqrl_verboseDescription);
+ }
+ }];
}

View File

@@ -32,8 +32,7 @@ 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)}%`);
console.log(`Effective cache hit rate: ${(hitRate * 100).toFixed(2)}%`);
if (uploadStats) {
if (!process.env.DD_API_KEY) {

View File

@@ -14,7 +14,6 @@ const args = minimist(process.argv.slice(2), {
const BASE = path.resolve(__dirname, '../..');
const ROOT_PACKAGE_JSON = path.resolve(BASE, 'package.json');
const NODE_DIR = path.resolve(BASE, 'third_party', 'electron_node');
const JUNIT_DIR = args.jUnitDir ? path.resolve(args.jUnitDir) : null;
const TAP_FILE_NAME = 'test.tap';
@@ -39,18 +38,6 @@ const defaultOptions = [
'-J'
];
// The root package.json is ESM, which breaks the test runner.
// Temporarily change it to CommonJS while running the tests, then
// change it back when done.
const resetPackageJson = ({ useESM }) => {
// This won't always exist in CI.
if (!fs.existsSync(ROOT_PACKAGE_JSON)) { return; }
const packageJson = JSON.parse(fs.readFileSync(ROOT_PACKAGE_JSON, 'utf-8'));
packageJson.type = useESM ? 'module' : 'commonjs';
fs.writeFileSync(ROOT_PACKAGE_JSON, JSON.stringify(packageJson, null, 2) + '\n');
};
const getCustomOptions = () => {
let customOptions = ['tools/test.py'];
@@ -92,8 +79,6 @@ async function main () {
const options = args.default ? defaultOptions : getCustomOptions();
resetPackageJson({ useESM: false });
const testChild = cp.spawn('python3', options, {
env: {
...process.env,
@@ -103,10 +88,7 @@ async function main () {
cwd: NODE_DIR,
stdio: 'inherit'
});
testChild.on('exit', (testCode) => {
resetPackageJson({ useESM: true });
if (JUNIT_DIR) {
fs.mkdirSync(JUNIT_DIR);
const converterStream = require('tap-xunit')();

View File

@@ -317,12 +317,6 @@ void BaseWindow::OnWindowSheetEnd() {
Emit("sheet-end");
}
void BaseWindow::OnWindowIsKeyChanged(bool is_key) {
#if BUILDFLAG(IS_MAC)
window()->SetActive(is_key);
#endif
}
void BaseWindow::OnWindowEnterHtmlFullScreen() {
Emit("enter-html-full-screen");
}

View File

@@ -85,7 +85,6 @@ class BaseWindow : public gin_helper::TrackableObject<BaseWindow>,
void OnWindowRotateGesture(float rotation) override;
void OnWindowSheetBegin() override;
void OnWindowSheetEnd() override;
void OnWindowIsKeyChanged(bool is_key) override;
void OnWindowEnterFullScreen() override;
void OnWindowLeaveFullScreen() override;
void OnWindowEnterHtmlFullScreen() override;

View File

@@ -280,22 +280,16 @@ v8::Local<v8::Value> BrowserWindow::GetWebContents(v8::Isolate* isolate) {
}
void BrowserWindow::OnWindowShow() {
if (!web_contents_shown_) {
web_contents()->WasShown();
web_contents_shown_ = true;
}
BaseWindow::OnWindowShow();
}
void BrowserWindow::OnWindowHide() {
web_contents()->WasOccluded();
web_contents_shown_ = false;
BaseWindow::OnWindowHide();
}
void BrowserWindow::Show() {
web_contents()->WasShown();
web_contents_shown_ = true;
BaseWindow::Show();
}
@@ -304,7 +298,6 @@ void BrowserWindow::ShowInactive() {
if (IsModal())
return;
web_contents()->WasShown();
web_contents_shown_ = true;
BaseWindow::ShowInactive();
}

View File

@@ -80,7 +80,6 @@ class BrowserWindow : public BaseWindow,
// Helpers.
v8::Global<v8::Value> web_contents_;
bool web_contents_shown_ = false;
v8::Global<v8::Value> web_contents_view_;
base::WeakPtr<api::WebContents> api_web_contents_;

View File

@@ -259,7 +259,7 @@ void UtilityProcessWrapper::OnServiceProcessLaunch(
EmitWithoutEvent("spawn");
}
void UtilityProcessWrapper::HandleTermination(uint32_t exit_code) {
void UtilityProcessWrapper::HandleTermination(uint64_t exit_code) {
// HandleTermination is called from multiple callsites,
// we need to ensure we only process it for the first callsite.
if (terminated_)
@@ -327,7 +327,7 @@ void UtilityProcessWrapper::CloseConnectorPort() {
}
}
void UtilityProcessWrapper::Shutdown(uint32_t exit_code) {
void UtilityProcessWrapper::Shutdown(uint64_t exit_code) {
node_service_remote_.reset();
HandleTermination(exit_code);
}

View File

@@ -57,7 +57,7 @@ class UtilityProcessWrapper final
static gin_helper::Handle<UtilityProcessWrapper> Create(gin::Arguments* args);
static raw_ptr<UtilityProcessWrapper> FromProcessId(base::ProcessId pid);
void Shutdown(uint32_t exit_code);
void Shutdown(uint64_t exit_code);
// gin_helper::Wrappable
static gin::DeprecatedWrapperInfo kWrapperInfo;
@@ -77,7 +77,7 @@ class UtilityProcessWrapper final
void OnServiceProcessLaunch(const base::Process& process);
void CloseConnectorPort();
void HandleTermination(uint32_t exit_code);
void HandleTermination(uint64_t exit_code);
void PostMessage(gin::Arguments* args);
bool Kill();

View File

@@ -50,7 +50,6 @@
#include "content/public/browser/context_menu_params.h"
#include "content/public/browser/desktop_media_id.h"
#include "content/public/browser/desktop_streams_registry.h"
#include "content/public/browser/devtools_agent_host.h"
#include "content/public/browser/download_request_utils.h"
#include "content/public/browser/favicon_status.h"
#include "content/public/browser/file_select_listener.h"
@@ -1312,11 +1311,10 @@ void WebContents::MaybeOverrideCreateParamsForNewWindow(
dict.Get(options::kOffscreen, &is_offscreen) && is_offscreen);
if (is_offscreen) {
// Use a no-op callback here. The real OnPaint callback will be bound
// to the child WebContents in AddNewContents via SetCallback().
auto* view = new OffScreenWebContentsView(
false, offscreen_use_shared_texture_,
offscreen_shared_texture_pixel_format_, base::DoNothing());
offscreen_shared_texture_pixel_format_,
base::BindRepeating(&WebContents::OnPaint, base::Unretained(this)));
create_params->view = view;
create_params->delegate_view = view;
}
@@ -1344,15 +1342,6 @@ content::WebContents* WebContents::AddNewContents(
v8::HandleScope handle_scope(isolate);
auto api_web_contents = CreateAndTake(isolate, std::move(new_contents), type);
// Rebind the paint callback to the child WebContents. The
// OffScreenWebContentsView was initially created with the parent's OnPaint
// in MaybeOverrideCreateParamsForNewWindow, but the paint data
// belongs to the child.
if (auto* osr_view = api_web_contents->GetOffScreenWebContentsView()) {
osr_view->SetCallback(base::BindRepeating(&WebContents::OnPaint,
api_web_contents->GetWeakPtr()));
}
// We call RenderFrameCreated here as at this point the empty "about:blank"
// render frame has already been created. If the window never navigates again
// RenderFrameCreated won't be called and certain prefs like
@@ -2802,11 +2791,6 @@ std::string WebContents::GetMediaSourceID(
return id;
}
std::string WebContents::GetOrCreateDevToolsTargetId() {
auto agent_host = content::DevToolsAgentHost::GetOrCreateFor(web_contents());
return agent_host->GetId();
}
bool WebContents::IsCrashed() const {
return web_contents()->IsCrashed();
}
@@ -4673,8 +4657,6 @@ void WebContents::FillObjectTemplate(v8::Isolate* isolate,
&WebContents::SetWebRTCIPHandlingPolicy)
.SetMethod("setWebRTCUDPPortRange", &WebContents::SetWebRTCUDPPortRange)
.SetMethod("getMediaSourceId", &WebContents::GetMediaSourceID)
.SetMethod("getOrCreateDevToolsTargetId",
&WebContents::GetOrCreateDevToolsTargetId)
.SetMethod("getWebRTCIPHandlingPolicy",
&WebContents::GetWebRTCIPHandlingPolicy)
.SetMethod("getWebRTCUDPPortRange", &WebContents::GetWebRTCUDPPortRange)

View File

@@ -230,7 +230,6 @@ class WebContents final : public ExclusiveAccessContext,
v8::Local<v8::Value> GetWebRTCUDPPortRange(v8::Isolate* isolate) const;
void SetWebRTCUDPPortRange(gin::Arguments* args);
std::string GetMediaSourceID(content::WebContents* request_web_contents);
std::string GetOrCreateDevToolsTargetId();
bool IsCrashed() const;
void ForcefullyCrashRenderer();
void SetUserAgent(const std::string& user_agent);

View File

@@ -9,9 +9,7 @@
#include <utility>
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/path_service.h"
#include "base/strings/string_util.h"
#include "base/task/single_thread_task_runner.h"
#include "base/threading/thread_restrictions.h"
#include "chrome/common/chrome_paths.h"
@@ -73,29 +71,6 @@ Browser* Browser::Get() {
return ElectronBrowserMainParts::Get()->browser();
}
// static
bool Browser::IsValidProtocolScheme(const std::string& scheme) {
// RFC 3986 Section 3.1:
// scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
if (scheme.empty()) {
LOG(ERROR) << "Protocol scheme must not be empty";
return false;
}
if (!base::IsAsciiAlpha(scheme[0])) {
LOG(ERROR) << "Protocol scheme must start with an ASCII letter";
return false;
}
for (size_t i = 1; i < scheme.size(); ++i) {
const char c = scheme[i];
if (!base::IsAsciiAlpha(c) && !base::IsAsciiDigit(c) && c != '+' &&
c != '-' && c != '.') {
LOG(ERROR) << "Protocol scheme contains invalid character: '" << c << "'";
return false;
}
}
return true;
}
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX)
void Browser::Focus(gin::Arguments* args) {
// Focus on the first visible window.

View File

@@ -134,10 +134,6 @@ class Browser : private WindowListObserver {
void SetAppUserModelID(const std::wstring& name);
#endif
// Validate that a protocol scheme conforms to RFC 3986:
// scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
static bool IsValidProtocolScheme(const std::string& scheme);
// Remove the default protocol handler registry key
bool RemoveAsDefaultProtocolClient(const std::string& protocol,
gin::Arguments* args);

View File

@@ -103,19 +103,16 @@ void Browser::ClearRecentDocuments() {}
bool Browser::SetAsDefaultProtocolClient(const std::string& protocol,
gin::Arguments* args) {
if (!IsValidProtocolScheme(protocol))
return false;
return SetDefaultWebClient(protocol);
}
bool Browser::IsDefaultProtocolClient(const std::string& protocol,
gin::Arguments* args) {
if (!IsValidProtocolScheme(protocol))
return false;
auto env = base::Environment::Create();
if (protocol.empty())
return false;
std::vector<std::string> argv = {kXdgSettings, "check",
kXdgSettingsDefaultSchemeHandler, protocol};
if (std::optional<std::string> desktop_name = env->GetVar("CHROME_DESKTOP")) {

View File

@@ -233,7 +233,7 @@ bool Browser::RemoveAsDefaultProtocolClient(const std::string& protocol,
bool Browser::SetAsDefaultProtocolClient(const std::string& protocol,
gin::Arguments* args) {
if (!IsValidProtocolScheme(protocol))
if (protocol.empty())
return false;
NSString* identifier = [base::apple::MainBundle() bundleIdentifier];
@@ -249,7 +249,7 @@ bool Browser::SetAsDefaultProtocolClient(const std::string& protocol,
bool Browser::IsDefaultProtocolClient(const std::string& protocol,
gin::Arguments* args) {
if (!IsValidProtocolScheme(protocol))
if (protocol.empty())
return false;
NSString* identifier = [base::apple::MainBundle() bundleIdentifier];

View File

@@ -429,7 +429,7 @@ bool Browser::SetUserTasks(const std::vector<UserTask>& tasks) {
bool Browser::RemoveAsDefaultProtocolClient(const std::string& protocol,
gin::Arguments* args) {
if (!IsValidProtocolScheme(protocol))
if (protocol.empty())
return false;
// Main Registry Key
@@ -508,7 +508,7 @@ bool Browser::SetAsDefaultProtocolClient(const std::string& protocol,
// Software\Classes", which is inherited by "HKEY_CLASSES_ROOT"
// anyway, and can be written by unprivileged users.
if (!IsValidProtocolScheme(protocol))
if (protocol.empty())
return false;
std::wstring exe;
@@ -538,7 +538,7 @@ bool Browser::SetAsDefaultProtocolClient(const std::string& protocol,
bool Browser::IsDefaultProtocolClient(const std::string& protocol,
gin::Arguments* args) {
if (!IsValidProtocolScheme(protocol))
if (protocol.empty())
return false;
std::wstring exe;

View File

@@ -87,9 +87,13 @@ HidChooserController::HidChooserController(
exclusion_filters_(std::move(exclusion_filters)),
callback_(std::move(callback)),
initiator_document_(render_frame_host->GetWeakDocumentPtr()),
origin_(render_frame_host->GetLastCommittedOrigin()),
origin_(content::WebContents::FromRenderFrameHost(render_frame_host)
->GetPrimaryMainFrame()
->GetLastCommittedOrigin()),
hid_delegate_(hid_delegate),
render_frame_host_id_(render_frame_host->GetGlobalId()) {
// The use above of GetMainFrame is safe as content::HidService instances are
// not created for fenced frames.
DCHECK(!render_frame_host->IsNestedWithinFencedFrame());
chooser_context_ = HidChooserContextFactory::GetForBrowserContext(

View File

@@ -170,12 +170,6 @@ class NativeWindowMac : public NativeWindow,
void NotifyWindowDidFailToEnterFullScreen();
void NotifyWindowWillLeaveFullScreen();
// Hide/show traffic light buttons around miniaturize/deminiaturize to
// prevent them from flashing at the default position during the restore
// animation when a custom trafficLightPosition is configured.
void HideTrafficLights();
void RestoreTrafficLights();
// Cleanup observers when window is getting closed. Note that the destructor
// can be called much later after window gets closed, so we should not do
// cleanup in destructor.

View File

@@ -1533,18 +1533,6 @@ void NativeWindowMac::RedrawTrafficLights() {
[buttons_proxy_ redraw];
}
void NativeWindowMac::HideTrafficLights() {
if (buttons_proxy_)
[buttons_proxy_ setVisible:NO];
}
void NativeWindowMac::RestoreTrafficLights() {
if (buttons_proxy_ && window_button_visibility_.value_or(true)) {
[buttons_proxy_ redraw];
[buttons_proxy_ setVisible:YES];
}
}
// In simpleFullScreen mode, update the frame for new bounds.
void NativeWindowMac::UpdateFrame() {
NSWindow* window = GetNativeWindow().GetNativeNSWindow();

View File

@@ -1017,13 +1017,17 @@ void NativeWindowViews::MoveTop() {
bool NativeWindowViews::CanResize() const {
#if BUILDFLAG(IS_WIN)
return has_frame() ? resizable_ && thick_frame_ : resizable_;
return resizable_ && thick_frame_;
#else
return resizable_;
#endif
}
bool NativeWindowViews::IsResizable() const {
#if BUILDFLAG(IS_WIN)
if (has_frame())
return ::GetWindowLong(GetAcceleratedWidget(), GWL_STYLE) & WS_THICKFRAME;
#endif
return CanResize();
}

View File

@@ -45,10 +45,6 @@ void OffScreenWebContentsView::SetWebContents(
view->InstallTransparency();
}
void OffScreenWebContentsView::SetCallback(const OnPaintCallback& callback) {
callback_ = callback;
}
void OffScreenWebContentsView::SetNativeWindow(NativeWindow* window) {
if (native_window_)
native_window_->RemoveObserver(this);

View File

@@ -43,7 +43,6 @@ class OffScreenWebContentsView : public content::WebContentsView,
void SetWebContents(content::WebContents*);
void SetNativeWindow(NativeWindow* window);
void SetCallback(const OnPaintCallback& callback);
// NativeWindowObserver:
void OnWindowResize() override;

View File

@@ -52,21 +52,25 @@ bool ElectronSerialDelegate::CanRequestPortPermission(
auto* permission_helper =
WebContentsPermissionHelper::FromWebContents(web_contents);
return permission_helper->CheckSerialAccessPermission(
frame->GetLastCommittedOrigin());
web_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin());
}
bool ElectronSerialDelegate::HasPortPermission(
content::RenderFrameHost* frame,
const device::mojom::SerialPortInfo& port) {
auto* web_contents = content::WebContents::FromRenderFrameHost(frame);
return GetChooserContext(frame)->HasPortPermission(
frame->GetLastCommittedOrigin(), port, frame);
web_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin(), port,
frame);
}
void ElectronSerialDelegate::RevokePortPermissionWebInitiated(
content::RenderFrameHost* frame,
const base::UnguessableToken& token) {
auto* web_contents = content::WebContents::FromRenderFrameHost(frame);
return GetChooserContext(frame)->RevokePortPermissionWebInitiated(
frame->GetLastCommittedOrigin(), token, frame);
web_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin(), token,
frame);
}
const device::mojom::SerialPortInfo* ElectronSerialDelegate::GetPortInfo(

View File

@@ -125,7 +125,7 @@ SerialChooserController::SerialChooserController(
std::move(allowed_bluetooth_service_class_ids)),
callback_(std::move(callback)),
initiator_document_(render_frame_host->GetWeakDocumentPtr()) {
origin_ = render_frame_host->GetLastCommittedOrigin();
origin_ = web_contents_->GetPrimaryMainFrame()->GetLastCommittedOrigin();
chooser_context_ = SerialChooserContextFactory::GetForBrowserContext(
web_contents_->GetBrowserContext())

View File

@@ -87,8 +87,8 @@ MouseDownImpl g_nsnextstepframe_mousedown;
(electron::NativeWindowMac*)[(id)self.window shell];
if (shell && !shell->has_frame())
[self cr_mouseDownOnFrameView:event];
g_nsthemeframe_mousedown(self, @selector(mouseDown:), event);
}
g_nsthemeframe_mousedown(self, @selector(mouseDown:), event);
}
- (void)swiz_nsnextstepframe_mouseDown:(NSEvent*)event {
@@ -98,8 +98,8 @@ MouseDownImpl g_nsnextstepframe_mousedown;
if (shell && !shell->has_frame()) {
[self cr_mouseDownOnFrameView:event];
}
g_nsnextstepframe_mousedown(self, @selector(mouseDown:), event);
}
g_nsnextstepframe_mousedown(self, @selector(mouseDown:), event);
}
- (void)swiz_nsview_swipeWithEvent:(NSEvent*)event {

View File

@@ -256,10 +256,6 @@ using TitleBarStyle = electron::NativeWindowMac::TitleBarStyle;
shell_->SetWindowLevel(NSNormalWindowLevel);
shell_->UpdateWindowOriginalFrame();
shell_->DetachChildren();
// Hide the traffic light buttons container before miniaturize so that
// when the window is restored, macOS does not render the buttons at
// their default position during the deminiaturize animation.
shell_->HideTrafficLights();
}
- (void)windowDidMiniaturize:(NSNotification*)notification {
@@ -277,10 +273,6 @@ using TitleBarStyle = electron::NativeWindowMac::TitleBarStyle;
shell_->set_wants_to_be_visible(true);
shell_->AttachChildren();
shell_->SetWindowLevel(level_);
// Reposition traffic light buttons and make them visible again.
// They were hidden in windowWillMiniaturize to prevent a flash at
// the default (0,0) position during the restore animation.
shell_->RestoreTrafficLights();
shell_->NotifyWindowRestore();
}

View File

@@ -243,8 +243,8 @@ void ElectronDesktopWindowTreeHostLinux::UpdateFrameHints() {
// The opaque region is a list of rectangles that contain only fully
// opaque pixels of the window. We need to convert the clipping
// rounded-rect into this format.
SkRRect rrect = layout->GetRoundedWindowBounds();
gfx::RectF rectf(layout->GetWindowBounds());
SkRRect rrect = layout->GetRoundedWindowContentBounds();
gfx::RectF rectf(layout->GetWindowContentBounds());
rectf.Scale(scale);
// It is acceptable to omit some pixels that are opaque, but the region
// must not include any translucent pixels. Therefore, we must

View File

@@ -158,7 +158,7 @@ DialogResult ShowTaskDialogWstr(gfx::AcceleratedWidget parent,
config.hInstance = GetModuleHandle(nullptr);
config.dwFlags = flags;
if (parent && ::IsWindowEnabled(parent)) {
if (parent) {
config.hwndParent = parent;
config.dwFlags |= TDF_POSITION_RELATIVE_TO_WINDOW;
}

View File

@@ -112,7 +112,7 @@ ClientFrameViewLinux::~ClientFrameViewLinux() {
void ClientFrameViewLinux::Init(NativeWindowViews* window,
views::Widget* frame) {
FramelessView::Init(window, frame);
linux_frame_layout_ = std::make_unique<LinuxCSDNativeFrameLayout>(window);
linux_frame_layout_ = std::make_unique<LinuxCSDFrameLayout>(window);
// Unretained() is safe because the subscription is saved into an instance
// member and thus will be cancelled upon the instance's destruction.
@@ -156,8 +156,7 @@ void ClientFrameViewLinux::OnWindowButtonOrderingChange() {
}
int ClientFrameViewLinux::ResizingBorderHitTest(const gfx::Point& point) {
return ResizingBorderHitTestImpl(
point, linux_frame_layout_->GetResizeBorderInsets());
return ResizingBorderHitTestImpl(point, RestoredFrameBorderInsets());
}
gfx::Rect ClientFrameViewLinux::GetBoundsForClientView() const {
@@ -236,11 +235,8 @@ void ClientFrameViewLinux::Layout(PassKey) {
}
void ClientFrameViewLinux::OnPaint(gfx::Canvas* canvas) {
if (auto* frame_provider = linux_frame_layout_->GetFrameProvider()) {
frame_provider->PaintWindowFrame(
canvas, GetLocalBounds(), GetTitlebarBounds().bottom(),
ShouldPaintAsActive(), linux_frame_layout_->GetInputInsets());
}
linux_frame_layout_->PaintWindowFrame(
canvas, GetLocalBounds(), GetTitlebarBounds(), ShouldPaintAsActive());
}
void ClientFrameViewLinux::PaintAsActiveChanged() {
@@ -271,7 +267,7 @@ void ClientFrameViewLinux::UpdateThemeValues() {
}
theme_values_.window_border_radius =
linux_frame_layout_->GetTopCornerRadiusDip();
linux_frame_layout_->GetFrameProvider()->GetTopCornerRadiusDip();
gtk::GtkStyleContextGet(headerbar_context, "min-height",
&theme_values_.titlebar_min_height, nullptr);

View File

@@ -112,7 +112,7 @@ class ClientFrameViewLinux : public FramelessView,
gfx::Insets GetTitlebarContentInsets() const;
gfx::Rect GetTitlebarContentBounds() const;
std::unique_ptr<LinuxCSDNativeFrameLayout> linux_frame_layout_;
std::unique_ptr<LinuxFrameLayout> linux_frame_layout_;
raw_ptr<ui::NativeTheme> theme_;
ThemeValues theme_values_;

View File

@@ -4,20 +4,14 @@
// found in the LICENSE file.
#include "shell/browser/ui/views/linux_frame_layout.h"
#include <algorithm>
#include "base/i18n/rtl.h"
#include "chrome/browser/ui/views/frame/browser_frame_view_paint_utils_linux.h" // nogncheck
#include "shell/browser/linux/x11_util.h"
#include "shell/browser/native_window_views.h"
#include "shell/browser/ui/electron_desktop_window_tree_host_linux.h"
#include "third_party/skia/include/core/SkRRect.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/skia_conversions.h"
#include "ui/linux/linux_ui.h"
#include "ui/linux/window_frame_provider.h"
#include "ui/views/layout/layout_provider.h"
#include "ui/native_theme/native_theme.h"
#include "ui/views/widget/widget.h"
namespace electron {
@@ -27,174 +21,151 @@ namespace {
constexpr int kResizeBorder = 10;
// This should match FramelessView's inside resize band.
constexpr int kResizeInsideBoundsSize = 5;
// These should match Chromium's restored frame edge thickness.
constexpr gfx::Insets kDefaultCustomFrameBorder = gfx::Insets::TLBR(2, 1, 1, 1);
bool CheckClientFrameShadowSupport(NativeWindowViews* window) {
auto* tree_host = static_cast<ElectronDesktopWindowTreeHostLinux*>(
ElectronDesktopWindowTreeHostLinux::GetHostForWidget(
window->GetAcceleratedWidget()));
return tree_host && tree_host->SupportsClientFrameShadow();
}
} // namespace
// static
std::unique_ptr<LinuxFrameLayout> LinuxFrameLayout::Create(
NativeWindowViews* window,
bool wants_shadow,
CSDStyle csd_style) {
bool wants_shadow) {
if (x11_util::IsX11() || window->IsTranslucent() || !wants_shadow) {
return std::make_unique<LinuxFrameLayout>(window);
} else if (csd_style == CSDStyle::kCustom) {
return std::make_unique<LinuxCSDCustomFrameLayout>(window);
return std::make_unique<LinuxUndecoratedFrameLayout>(window);
} else {
return std::make_unique<LinuxCSDNativeFrameLayout>(window);
return std::make_unique<LinuxCSDFrameLayout>(window);
}
}
gfx::Insets LinuxFrameLayout::GetResizeBorderInsets() const {
gfx::Insets insets = RestoredFrameBorderInsets();
return insets.IsEmpty() ? GetInputInsets() : insets;
}
SkRRect LinuxFrameLayout::GetRoundedWindowBounds() const {
SkRect rect = gfx::RectToSkRect(GetWindowBounds());
SkRRect rrect;
float radius = GetTopCornerRadiusDip();
if (radius > 0) {
SkPoint round_point{radius, radius};
SkPoint radii[] = {round_point, round_point, {}, {}};
rrect.setRectRadii(rect, radii);
} else {
rrect.setRect(rect);
}
return rrect;
}
// Base implementation is suitable for X11/views without shadows
LinuxFrameLayout::LinuxFrameLayout(NativeWindowViews* window)
LinuxCSDFrameLayout::LinuxCSDFrameLayout(NativeWindowViews* window)
: window_(window) {
host_supports_client_frame_shadow_ = false;
host_supports_client_frame_shadow_ = SupportsClientFrameShadow();
}
LinuxFrameLayout::~LinuxFrameLayout() = default;
gfx::Insets LinuxFrameLayout::RestoredFrameBorderInsets() const {
return gfx::Insets();
}
gfx::Insets LinuxFrameLayout::GetInputInsets() const {
return gfx::Insets(kResizeInsideBoundsSize);
}
bool LinuxFrameLayout::IsShowingShadow() const {
return host_supports_client_frame_shadow_ && !window_->IsMaximized() &&
!window_->IsFullscreen();
}
bool LinuxFrameLayout::SupportsClientFrameShadow() const {
return host_supports_client_frame_shadow_;
}
bool LinuxFrameLayout::tiled() const {
bool LinuxCSDFrameLayout::tiled() const {
return tiled_;
}
void LinuxFrameLayout::set_tiled(bool tiled) {
void LinuxCSDFrameLayout::set_tiled(bool tiled) {
tiled_ = tiled;
}
gfx::Rect LinuxFrameLayout::GetWindowBounds() const {
gfx::Rect bounds = window_->widget()->GetWindowBoundsInScreen();
bounds.Inset(RestoredFrameBorderInsets());
return bounds;
}
gfx::Insets LinuxCSDFrameLayout::RestoredFrameBorderInsets() const {
gfx::Insets insets = GetFrameProvider()->GetFrameThicknessDip();
const gfx::Insets input = GetInputInsets();
float LinuxFrameLayout::GetTopCornerRadiusDip() const {
return 0;
}
int LinuxFrameLayout::GetTranslucentTopAreaHeight() const {
return 0;
}
gfx::Insets LinuxFrameLayout::NormalizeBorderInsets(
const gfx::Insets& frame_insets,
const gfx::Insets& input_insets) const {
auto expand_if_visible = [](int side_thickness, int min_band) {
return side_thickness > 0 ? std::max(side_thickness, min_band) : 0;
};
// Ensure hit testing for resize targets works
// even if borders/shadows are absent on some edges.
gfx::Insets merged;
merged.set_top(expand_if_visible(frame_insets.top(), input_insets.top()));
merged.set_left(expand_if_visible(frame_insets.left(), input_insets.left()));
merged.set_bottom(
expand_if_visible(frame_insets.bottom(), input_insets.bottom()));
merged.set_right(
expand_if_visible(frame_insets.right(), input_insets.right()));
merged.set_top(expand_if_visible(insets.top(), input.top()));
merged.set_left(expand_if_visible(insets.left(), input.left()));
merged.set_bottom(expand_if_visible(insets.bottom(), input.bottom()));
merged.set_right(expand_if_visible(insets.right(), input.right()));
return base::i18n::IsRTL() ? gfx::Insets::TLBR(merged.top(), merged.right(),
merged.bottom(), merged.left())
: merged;
}
// Used for a native-like frame with a FrameProvider
LinuxCSDNativeFrameLayout::LinuxCSDNativeFrameLayout(NativeWindowViews* window)
: LinuxFrameLayout(window) {
host_supports_client_frame_shadow_ = CheckClientFrameShadowSupport(window);
gfx::Insets LinuxCSDFrameLayout::GetInputInsets() const {
bool showing_shadow = host_supports_client_frame_shadow_ &&
!window_->IsMaximized() && !window_->IsFullscreen();
return gfx::Insets(showing_shadow ? kResizeBorder : 0);
}
LinuxCSDNativeFrameLayout::~LinuxCSDNativeFrameLayout() = default;
gfx::Insets LinuxCSDNativeFrameLayout::RestoredFrameBorderInsets() const {
const gfx::Insets input_insets = GetInputInsets();
const gfx::Insets frame_insets = GetFrameProvider()->GetFrameThicknessDip();
return NormalizeBorderInsets(frame_insets, input_insets);
bool LinuxCSDFrameLayout::SupportsClientFrameShadow() const {
auto* tree_host = static_cast<ElectronDesktopWindowTreeHostLinux*>(
ElectronDesktopWindowTreeHostLinux::GetHostForWidget(
window_->GetAcceleratedWidget()));
return tree_host->SupportsClientFrameShadow();
}
gfx::Insets LinuxCSDNativeFrameLayout::GetInputInsets() const {
return gfx::Insets(IsShowingShadow() ? kResizeBorder : 0);
void LinuxCSDFrameLayout::PaintWindowFrame(gfx::Canvas* canvas,
gfx::Rect local_bounds,
gfx::Rect titlebar_bounds,
bool active) {
GetFrameProvider()->PaintWindowFrame(
canvas, local_bounds, titlebar_bounds.bottom(), active, GetInputInsets());
}
float LinuxCSDNativeFrameLayout::GetTopCornerRadiusDip() const {
return window_->IsMaximized() ? 0
: GetFrameProvider()->GetTopCornerRadiusDip();
gfx::Rect LinuxCSDFrameLayout::GetWindowContentBounds() const {
gfx::Rect content_bounds = window_->widget()->GetWindowBoundsInScreen();
content_bounds.Inset(RestoredFrameBorderInsets());
return content_bounds;
}
ui::WindowFrameProvider* LinuxCSDNativeFrameLayout::GetFrameProvider() const {
SkRRect LinuxCSDFrameLayout::GetRoundedWindowContentBounds() const {
SkRect rect = gfx::RectToSkRect(GetWindowContentBounds());
SkRRect rrect;
if (!window_->IsMaximized()) {
float radius = GetFrameProvider()->GetTopCornerRadiusDip();
SkPoint round_point{radius, radius};
SkPoint radii[] = {round_point, round_point, {}, {}};
rrect.setRectRadii(rect, radii);
} else {
rrect.setRect(rect);
}
return rrect;
}
int LinuxCSDFrameLayout::GetTranslucentTopAreaHeight() const {
return 0;
}
ui::WindowFrameProvider* LinuxCSDFrameLayout::GetFrameProvider() const {
return ui::LinuxUiTheme::GetForProfile(nullptr)->GetWindowFrameProvider(
!host_supports_client_frame_shadow_, tiled(), window_->IsMaximized());
}
// Used for Chromium-like custom CSD
LinuxCSDCustomFrameLayout::LinuxCSDCustomFrameLayout(NativeWindowViews* window)
: LinuxFrameLayout(window) {
host_supports_client_frame_shadow_ = CheckClientFrameShadowSupport(window);
LinuxUndecoratedFrameLayout::LinuxUndecoratedFrameLayout(
NativeWindowViews* window)
: window_(window) {}
gfx::Insets LinuxUndecoratedFrameLayout::RestoredFrameBorderInsets() const {
return gfx::Insets();
}
LinuxCSDCustomFrameLayout::~LinuxCSDCustomFrameLayout() = default;
gfx::Insets LinuxCSDCustomFrameLayout::RestoredFrameBorderInsets() const {
const gfx::Insets input_insets = GetInputInsets();
const bool showing_shadow = IsShowingShadow();
const auto shadow_values = (showing_shadow && !tiled())
? GetFrameShadowValuesLinux(/*active=*/true)
: gfx::ShadowValues();
const gfx::Insets frame_insets = GetRestoredFrameBorderInsetsLinux(
showing_shadow, kDefaultCustomFrameBorder, shadow_values, input_insets);
return NormalizeBorderInsets(frame_insets, input_insets);
gfx::Insets LinuxUndecoratedFrameLayout::GetInputInsets() const {
return gfx::Insets(kResizeInsideBoundsSize);
}
gfx::Insets LinuxCSDCustomFrameLayout::GetInputInsets() const {
return gfx::Insets(IsShowingShadow() ? kResizeBorder : 0);
bool LinuxUndecoratedFrameLayout::SupportsClientFrameShadow() const {
return false;
}
gfx::ShadowValues GetFrameShadowValuesLinux(bool active) {
const int elevation = views::LayoutProvider::Get()->GetShadowElevationMetric(
active ? views::Emphasis::kMaximum : views::Emphasis::kMedium);
return gfx::ShadowValue::MakeMdShadowValues(elevation);
bool LinuxUndecoratedFrameLayout::tiled() const {
return tiled_;
}
void LinuxUndecoratedFrameLayout::set_tiled(bool tiled) {
tiled_ = tiled;
}
void LinuxUndecoratedFrameLayout::PaintWindowFrame(gfx::Canvas* canvas,
gfx::Rect local_bounds,
gfx::Rect titlebar_bounds,
bool active) {
// No-op
}
gfx::Rect LinuxUndecoratedFrameLayout::GetWindowContentBounds() const {
// With no transparent insets, widget bounds and logical bounds match.
return window_->widget()->GetWindowBoundsInScreen();
}
SkRRect LinuxUndecoratedFrameLayout::GetRoundedWindowContentBounds() const {
SkRRect rrect;
rrect.setRect(gfx::RectToSkRect(GetWindowContentBounds()));
return rrect;
}
int LinuxUndecoratedFrameLayout::GetTranslucentTopAreaHeight() const {
return 0;
}
ui::WindowFrameProvider* LinuxUndecoratedFrameLayout::GetFrameProvider() const {
return nullptr;
}
} // namespace electron

View File

@@ -8,96 +8,110 @@
#include <memory>
#include "base/memory/raw_ptr.h"
#include "base/i18n/rtl.h"
#include "shell/browser/linux/x11_util.h"
#include "shell/browser/native_window_views.h"
#include "shell/browser/ui/electron_desktop_window_tree_host_linux.h"
#include "third_party/skia/include/core/SkRRect.h"
#include "ui/gfx/shadow_value.h"
#include "ui/base/ozone_buildflags.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/linux/linux_ui.h"
#include "ui/linux/window_frame_provider.h"
namespace gfx {
class Insets;
class Rect;
} // namespace gfx
namespace electron {
class NativeWindowViews;
// Shared helper for CSD layout on Linux (shadows, resize regions, titlebars,
// etc.). Also helps views determine insets and perform bounds conversions
// between widget and logical coordinates.
//
// The base class is concrete and suitable as-is for the undecorated case (X11,
// translucent windows, or windows without shadows). CSD subclasses override
// the methods that differ.
// Shared helper for CSD layout and frame painting on Linux (shadows, resize
// regions, titlebars, etc.). Also helps views determine insets and perform
// bounds conversions between widget and logical coordinates.
class LinuxFrameLayout {
public:
enum class CSDStyle {
kNativeFrame,
kCustom,
};
explicit LinuxFrameLayout(NativeWindowViews* window);
virtual ~LinuxFrameLayout();
virtual ~LinuxFrameLayout() = default;
static std::unique_ptr<LinuxFrameLayout> Create(NativeWindowViews* window,
bool wants_shadow,
CSDStyle csd_style);
bool wants_shadow);
// Insets from the transparent widget border to the opaque part of the window.
virtual gfx::Insets RestoredFrameBorderInsets() const;
// Insets for parts of the surface that should be counted for user input.
virtual gfx::Insets GetInputInsets() const;
// Insets to use for non-client resize hit-testing.
gfx::Insets GetResizeBorderInsets() const;
// Insets from the transparent widget border to the opaque part of the window
virtual gfx::Insets RestoredFrameBorderInsets() const = 0;
// Insets for parts of the surface that should be counted for user input
virtual gfx::Insets GetInputInsets() const = 0;
bool IsShowingShadow() const;
bool SupportsClientFrameShadow() const;
virtual bool SupportsClientFrameShadow() const = 0;
bool tiled() const;
void set_tiled(bool tiled);
virtual bool tiled() const = 0;
virtual void set_tiled(bool tiled) = 0;
// The logical bounds of the window interior.
gfx::Rect GetWindowBounds() const;
// The logical window bounds as a rounded rect with corner radii applied.
SkRRect GetRoundedWindowBounds() const;
// The corner radius of the top corners of the window, in DIPs.
virtual float GetTopCornerRadiusDip() const;
virtual void PaintWindowFrame(gfx::Canvas* canvas,
gfx::Rect local_bounds,
gfx::Rect titlebar_bounds,
bool active) = 0;
int GetTranslucentTopAreaHeight() const;
// The logical bounds of the window
virtual gfx::Rect GetWindowContentBounds() const = 0;
// The logical bounds as a rounded rect with corner radii applied
virtual SkRRect GetRoundedWindowContentBounds() const = 0;
protected:
gfx::Insets NormalizeBorderInsets(const gfx::Insets& frame_insets,
const gfx::Insets& input_insets) const;
virtual int GetTranslucentTopAreaHeight() const = 0;
virtual ui::WindowFrameProvider* GetFrameProvider() const = 0;
};
// Client-side decoration (CSD) Linux frame layout implementation.
class LinuxCSDFrameLayout : public LinuxFrameLayout {
public:
explicit LinuxCSDFrameLayout(NativeWindowViews* window);
~LinuxCSDFrameLayout() override = default;
gfx::Insets RestoredFrameBorderInsets() const override;
gfx::Insets GetInputInsets() const override;
bool SupportsClientFrameShadow() const override;
bool tiled() const override;
void set_tiled(bool tiled) override;
void PaintWindowFrame(gfx::Canvas* canvas,
gfx::Rect local_bounds,
gfx::Rect titlebar_bounds,
bool active) override;
gfx::Rect GetWindowContentBounds() const override;
SkRRect GetRoundedWindowContentBounds() const override;
int GetTranslucentTopAreaHeight() const override;
ui::WindowFrameProvider* GetFrameProvider() const override;
private:
raw_ptr<NativeWindowViews> window_;
bool tiled_ = false;
bool host_supports_client_frame_shadow_ = false;
};
// CSD strategy that uses the GTK window frame provider for metrics.
class LinuxCSDNativeFrameLayout : public LinuxFrameLayout {
// No-decoration Linux frame layout implementation.
//
// Intended for cases where we do not allocate a transparent inset area around
// the window (e.g. X11 / server-side decorations, or when insets are disabled).
// All inset math returns 0 and frame painting is skipped.
class LinuxUndecoratedFrameLayout : public LinuxFrameLayout {
public:
explicit LinuxCSDNativeFrameLayout(NativeWindowViews* window);
~LinuxCSDNativeFrameLayout() override;
explicit LinuxUndecoratedFrameLayout(NativeWindowViews* window);
~LinuxUndecoratedFrameLayout() override = default;
gfx::Insets RestoredFrameBorderInsets() const override;
gfx::Insets GetInputInsets() const override;
float GetTopCornerRadiusDip() const override;
ui::WindowFrameProvider* GetFrameProvider() const;
bool SupportsClientFrameShadow() const override;
bool tiled() const override;
void set_tiled(bool tiled) override;
void PaintWindowFrame(gfx::Canvas* canvas,
gfx::Rect local_bounds,
gfx::Rect titlebar_bounds,
bool active) override;
gfx::Rect GetWindowContentBounds() const override;
SkRRect GetRoundedWindowContentBounds() const override;
int GetTranslucentTopAreaHeight() const override;
ui::WindowFrameProvider* GetFrameProvider() const override;
private:
raw_ptr<NativeWindowViews> window_;
bool tiled_ = false;
};
// CSD strategy that uses custom metrics, similar to those used in Chromium.
class LinuxCSDCustomFrameLayout : public LinuxFrameLayout {
public:
explicit LinuxCSDCustomFrameLayout(NativeWindowViews* window);
~LinuxCSDCustomFrameLayout() override;
gfx::Insets RestoredFrameBorderInsets() const override;
gfx::Insets GetInputInsets() const override;
};
gfx::ShadowValues GetFrameShadowValuesLinux(bool active);
} // namespace electron
#endif // ELECTRON_SHELL_BROWSER_UI_VIEWS_LINUX_FRAME_LAYOUT_H_

View File

@@ -5,24 +5,22 @@
#include "shell/browser/ui/views/opaque_frame_view.h"
#include "base/containers/adapters.h"
#include "chrome/browser/ui/views/frame/browser_frame_view_paint_utils_linux.h" // nogncheck
#include "base/i18n/rtl.h"
#include "chrome/browser/ui/views/frame/opaque_browser_frame_view_layout.h" // nogncheck
#include "chrome/grit/generated_resources.h"
#include "components/strings/grit/components_strings.h"
#include "shell/browser/native_window_views.h"
#include "shell/browser/ui/views/caption_button_placeholder_container.h"
#include "third_party/skia/include/core/SkRRect.h"
#include "ui/base/hit_test.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/font_list.h"
#include "ui/gfx/geometry/insets_f.h"
#include "ui/gfx/geometry/skia_conversions.h"
#include "ui/linux/linux_ui.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.h"
#include "ui/views/widget/widget.h"
#include "ui/views/window/frame_background.h"
#include "ui/views/widget/widget_delegate.h"
#include "ui/views/window/frame_caption_button.h"
#include "ui/views/window/vector_icons/vector_icons.h"
@@ -57,14 +55,12 @@ const int kCaptionButtonBottomPadding = 3;
// The content edge images have a shadow built into them.
const int OpaqueFrameView::kContentEdgeShadowThickness = 2;
OpaqueFrameView::OpaqueFrameView()
: frame_background_(std::make_unique<views::FrameBackground>()) {}
OpaqueFrameView::OpaqueFrameView() = default;
OpaqueFrameView::~OpaqueFrameView() = default;
void OpaqueFrameView::Init(NativeWindowViews* window, views::Widget* frame) {
FramelessView::Init(window, frame);
linux_frame_layout_ = LinuxFrameLayout::Create(
window, window->HasShadow(), LinuxFrameLayout::CSDStyle::kCustom);
linux_frame_layout_ = LinuxFrameLayout::Create(window, window->HasShadow());
// Unretained() is safe because the subscription is saved into an instance
// member and thus will be cancelled upon the instance's destruction.
@@ -102,8 +98,9 @@ void OpaqueFrameView::Init(NativeWindowViews* window, views::Widget* frame) {
}
int OpaqueFrameView::ResizingBorderHitTest(const gfx::Point& point) {
auto insets = RestoredFrameBorderInsets();
return ResizingBorderHitTestImpl(
point, linux_frame_layout_->GetResizeBorderInsets());
point, insets.IsEmpty() ? linux_frame_layout_->GetInputInsets() : insets);
}
void OpaqueFrameView::InvalidateCaptionButtons() {
@@ -203,31 +200,14 @@ void OpaqueFrameView::OnPaint(gfx::Canvas* canvas) {
if (frame()->IsFullscreen())
return;
const bool active = ShouldPaintAsActive();
const gfx::Insets border = RestoredFrameBorderInsets();
const bool showing_shadow = linux_frame_layout_->IsShowingShadow();
gfx::RectF bounds_dip(GetLocalBounds());
if (showing_shadow) {
bounds_dip.Inset(gfx::InsetsF(border));
}
// Titlebar height must be at least the frame border insets to avoid
// a negative height calculation in the GTK frame provider. We add 1 to
// ensure it's always positive even when insets are 0.
int top_area_height = RestoredFrameBorderInsets().top() + 1;
// TODO: support roundedCorners.
float radius_dip = 0;
SkVector radii[4]{{radius_dip, radius_dip}, {radius_dip, radius_dip}, {}, {}};
SkRRect clip;
clip.setRectRadii(gfx::RectFToSkRect(bounds_dip), radii);
frame_background_->set_frame_color(GetFrameColor());
frame_background_->set_use_custom_frame(true);
frame_background_->set_is_active(active);
frame_background_->set_top_area_height(GetTopAreaHeight());
const bool draw_shadow = showing_shadow && !linux_frame_layout_->tiled();
auto shadow_values =
draw_shadow ? GetFrameShadowValuesLinux(active) : gfx::ShadowValues();
::PaintRestoredFrameBorderLinux(*canvas, *this, frame_background_.get(), clip,
showing_shadow, active, border, shadow_values,
linux_frame_layout_->tiled());
linux_frame_layout_->PaintWindowFrame(
canvas, GetLocalBounds(), gfx::Rect(0, 0, width(), top_area_height),
ShouldPaintAsActive());
if (!window()->IsWindowControlsOverlayEnabled())
return;

View File

@@ -20,10 +20,6 @@
class CaptionButtonPlaceholderContainer;
namespace views {
class FrameBackground;
}
namespace electron {
class NativeWindowViews;
@@ -170,7 +166,6 @@ class OpaqueFrameView : public FramelessView {
bool is_leading_button) const;
std::unique_ptr<LinuxFrameLayout> linux_frame_layout_;
std::unique_ptr<views::FrameBackground> frame_background_;
// Window controls.
raw_ptr<views::Button> minimize_button_;

View File

@@ -43,7 +43,7 @@ UsbChooserController::UsbChooserController(
: WebContentsObserver(web_contents),
options_(std::move(options)),
callback_(std::move(callback)),
origin_(render_frame_host->GetLastCommittedOrigin()),
origin_(render_frame_host->GetMainFrame()->GetLastCommittedOrigin()),
usb_delegate_(usb_delegate),
render_frame_host_id_(render_frame_host->GetGlobalId()) {
chooser_context_ = UsbChooserContextFactory::GetForBrowserContext(

View File

@@ -219,7 +219,7 @@ void WebContentsPermissionHelper::RequestPermission(
base::DictValue details) {
auto* permission_manager = static_cast<ElectronPermissionManager*>(
web_contents_->GetBrowserContext()->GetPermissionControllerDelegate());
auto origin = requesting_frame->GetLastCommittedOrigin().GetURL();
auto origin = web_contents_->GetLastCommittedURL();
permission_manager->RequestPermissionWithDetails(
content::PermissionDescriptorUtil::
CreatePermissionDescriptorForPermissionType(permission),

View File

@@ -343,6 +343,9 @@ void WebContentsPreferences::AppendCommandLineSwitches(
command_line->AppendSwitchASCII(::switches::kDisableBlinkFeatures,
*disable_blink_features_);
if (node_integration_in_worker_)
command_line->AppendSwitch(switches::kNodeIntegrationInWorker);
// We are appending args to a webContents so let's save the current state
// of our preferences object so that during the lifetime of the WebContents
// we can fetch the options used to initially configure the WebContents

View File

@@ -277,6 +277,10 @@ inline constexpr base::cstring_view kAppPath = "app-path";
// The command line switch versions of the options.
inline constexpr base::cstring_view kScrollBounce = "scroll-bounce";
// Command switch passed to renderer process to control nodeIntegration.
inline constexpr base::cstring_view kNodeIntegrationInWorker =
"node-integration-in-worker";
// Widevine options
// Path to Widevine CDM binaries.
inline constexpr base::cstring_view kWidevineCdmPath = "widevine-cdm-path";

View File

@@ -6,6 +6,7 @@
#include <algorithm>
#include "base/command_line.h"
#include "content/public/renderer/render_frame.h"
#include "electron/fuses.h"
#include "net/http/http_request_headers.h"
@@ -15,6 +16,7 @@
#include "shell/common/node_bindings.h"
#include "shell/common/node_includes.h"
#include "shell/common/node_util.h"
#include "shell/common/options_switches.h"
#include "shell/common/v8_util.h"
#include "shell/renderer/electron_render_frame_observer.h"
#include "shell/renderer/web_worker_observer.h"
@@ -23,8 +25,6 @@
#include "third_party/blink/public/web/web_local_frame.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h" // nogncheck
#include "third_party/blink/renderer/core/frame/web_local_frame_impl.h" // nogncheck
#include "third_party/blink/renderer/core/workers/worker_global_scope.h" // nogncheck
#include "third_party/blink/renderer/core/workers/worker_settings.h" // nogncheck
namespace electron {
@@ -199,54 +199,44 @@ void ElectronRendererClient::WillReleaseScriptContext(
electron_bindings_->EnvironmentDestroyed(env);
}
namespace {
bool WorkerHasNodeIntegration(blink::ExecutionContext* ec) {
void ElectronRendererClient::WorkerScriptReadyForEvaluationOnWorkerThread(
v8::Local<v8::Context> context) {
// We do not create a Node.js environment in service or shared workers
// owing to an inability to customize sandbox policies in these workers
// given that they're run out-of-process.
// Also avoid creating a Node.js environment for worklet global scope
// created on the main thread.
auto* ec = blink::ExecutionContext::From(context);
if (ec->IsServiceWorkerGlobalScope() || ec->IsSharedWorkerGlobalScope() ||
ec->IsMainThreadWorkletGlobalScope())
return false;
auto* wgs = blink::DynamicTo<blink::WorkerGlobalScope>(ec);
if (!wgs)
return false;
// Read the nodeIntegrationInWorker preference from the worker's settings,
// which were copied from the initiating frame's WebPreferences at worker
// creation time. This ensures that in-process child windows with different
// webPreferences get the correct per-frame value rather than a process-wide
// value.
auto* worker_settings = wgs->GetWorkerSettings();
return worker_settings && worker_settings->NodeIntegrationInWorker();
}
} // namespace
void ElectronRendererClient::WorkerScriptReadyForEvaluationOnWorkerThread(
v8::Local<v8::Context> context) {
auto* ec = blink::ExecutionContext::From(context);
if (!WorkerHasNodeIntegration(ec))
return;
auto* current = WebWorkerObserver::GetCurrent();
if (current)
return;
WebWorkerObserver::Create()->WorkerScriptReadyForEvaluation(context);
// This won't be correct for in-process child windows with webPreferences
// that have a different value for nodeIntegrationInWorker
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kNodeIntegrationInWorker)) {
auto* current = WebWorkerObserver::GetCurrent();
if (current)
return;
WebWorkerObserver::Create()->WorkerScriptReadyForEvaluation(context);
}
}
void ElectronRendererClient::WillDestroyWorkerContextOnWorkerThread(
v8::Local<v8::Context> context) {
auto* ec = blink::ExecutionContext::From(context);
if (!WorkerHasNodeIntegration(ec))
if (ec->IsServiceWorkerGlobalScope() || ec->IsSharedWorkerGlobalScope() ||
ec->IsMainThreadWorkletGlobalScope())
return;
auto* current = WebWorkerObserver::GetCurrent();
if (current)
current->ContextWillDestroy(context);
// TODO(loc): Note that this will not be correct for in-process child windows
// with webPreferences that have a different value for nodeIntegrationInWorker
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kNodeIntegrationInWorker)) {
auto* current = WebWorkerObserver::GetCurrent();
if (current)
current->ContextWillDestroy(context);
}
}
void ElectronRendererClient::SetUpWebAssemblyTrapHandler() {

View File

@@ -1478,29 +1478,6 @@ describe('app module', () => {
});
});
describe('protocol scheme validation', () => {
it('rejects empty protocol names', () => {
expect(app.setAsDefaultProtocolClient('')).to.equal(false);
expect(app.isDefaultProtocolClient('')).to.equal(false);
expect(app.removeAsDefaultProtocolClient('')).to.equal(false);
});
it('rejects non-conformant protocol names ', () => {
// Starting with a digit.
expect(app.setAsDefaultProtocolClient('0badscheme')).to.equal(false);
// Starting with a hyphen.
expect(app.setAsDefaultProtocolClient('-badscheme')).to.equal(false);
// Containing backslashes.
expect(app.setAsDefaultProtocolClient('http\\shell\\open\\command')).to.equal(false);
// Containing forward slashes.
expect(app.setAsDefaultProtocolClient('bad/protocol')).to.equal(false);
// Containing spaces.
expect(app.setAsDefaultProtocolClient('bad protocol')).to.equal(false);
// Containing colons.
expect(app.setAsDefaultProtocolClient('bad:protocol')).to.equal(false);
});
});
ifdescribe(process.platform === 'win32')('app launch through uri', () => {
it('does not launch for argument following a URL', async () => {
const appPath = path.join(fixturesPath, 'api', 'quit-app');

View File

@@ -403,7 +403,7 @@ ifdescribe(shouldRunCodesignTests)('autoUpdater behavior', function () {
});
});
it('should preserve the staged update directory and prune orphaned ones when a new update is downloaded', async () => {
it('should clean up old staged update directories when a new update is downloaded', async () => {
// Clean up any existing update directories before the test
await cleanSquirrelCache();
@@ -419,23 +419,16 @@ ifdescribe(shouldRunCodesignTests)('autoUpdater behavior', function () {
}, async (_, updateZipPath3) => {
let updateCount = 0;
let downloadCount = 0;
let dirsDuringFirstDownload: string[] = [];
let dirsDuringSecondDownload: string[] = [];
let directoriesDuringSecondDownload: string[] = [];
server.get('/update-file', async (req, res) => {
downloadCount++;
// Snapshot update directories at the moment each download begins.
// By this point uniqueTemporaryDirectoryForUpdate has already run
// (prune + mkdtemp). We want to verify:
// 1st download: 1 dir (nothing to preserve, nothing to prune)
// 2nd download: 2 dirs (staged dir from 1st check is preserved
// so quitAndInstall stays safe, + new temp dir)
// The count never exceeds 2 across repeated checks — orphaned dirs
// (no longer referenced by ShipItState.plist) get pruned.
if (downloadCount === 1) {
dirsDuringFirstDownload = await getUpdateDirectoriesInCache();
} else if (downloadCount === 2) {
dirsDuringSecondDownload = await getUpdateDirectoriesInCache();
// When the second download request arrives, Squirrel has already
// called uniqueTemporaryDirectoryForUpdate which (with our patch)
// cleans up old directories before creating the new one.
// Without the patch, both directories would exist at this point.
if (downloadCount === 2) {
directoriesDuringSecondDownload = await getUpdateDirectoriesInCache();
}
res.download(updateCount > 1 ? updateZipPath3 : updateZipPath2);
});
@@ -462,181 +455,15 @@ ifdescribe(shouldRunCodesignTests)('autoUpdater behavior', function () {
await relaunchPromise;
// First download: exactly one temp dir (the first update).
expect(dirsDuringFirstDownload).to.have.lengthOf(1,
`Expected 1 update directory during first download but found ${dirsDuringFirstDownload.length}: ${dirsDuringFirstDownload.join(', ')}`);
// Second download: exactly two — the staged one preserved + the new
// one. Crucially the first download's directory must still be present,
// otherwise a mid-download quitAndInstall would find a dangling
// ShipItState.plist.
expect(dirsDuringSecondDownload).to.have.lengthOf(2,
`Expected 2 update directories during second download (staged + new) but found ${dirsDuringSecondDownload.length}: ${dirsDuringSecondDownload.join(', ')}`);
expect(dirsDuringSecondDownload).to.include(dirsDuringFirstDownload[0],
'The staged update directory from the first download must be preserved during the second download');
// During the second download, the old staged update directory should
// have been cleaned up. With our patch, there should be exactly 1
// directory (the new one). Without the patch, there would be 2.
expect(directoriesDuringSecondDownload).to.have.lengthOf(1,
`Expected 1 update directory during second download but found ${directoriesDuringSecondDownload.length}: ${directoriesDuringSecondDownload.join(', ')}`);
});
});
});
it('should keep the update directory count bounded across repeated checks', async () => {
// Verifies the orphan prune actually fires: after a second download
// completes and rewrites ShipItState.plist, the first directory is no
// longer referenced and must be removed when a third check begins.
// Without this, directories would accumulate forever.
await cleanSquirrelCache();
await withUpdatableApp({
nextVersion: '2.0.0',
startFixture: 'update-triple-stack',
endFixture: 'update-triple-stack'
}, async (appPath, updateZipPath2) => {
await withUpdatableApp({
nextVersion: '3.0.0',
startFixture: 'update-triple-stack',
endFixture: 'update-triple-stack'
}, async (_, updateZipPath3) => {
await withUpdatableApp({
nextVersion: '4.0.0',
startFixture: 'update-triple-stack',
endFixture: 'update-triple-stack'
}, async (__, updateZipPath4) => {
let downloadCount = 0;
const dirsPerDownload: string[][] = [];
server.get('/update-file', async (req, res) => {
downloadCount++;
// Snapshot after prune+mkdtemp but before the payload transfers.
dirsPerDownload.push(await getUpdateDirectoriesInCache());
const zips = [updateZipPath2, updateZipPath3, updateZipPath4];
res.download(zips[Math.min(downloadCount, zips.length) - 1]);
});
server.get('/update-check', (req, res) => {
res.json({
url: `http://localhost:${port}/update-file`,
name: 'My Release Name',
notes: 'Theses are some release notes innit',
pub_date: (new Date()).toString()
});
});
const relaunchPromise = new Promise<void>((resolve) => {
server.get('/update-check/updated/:version', (req, res) => {
res.status(204).send();
resolve();
});
});
const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
logOnError(launchResult, () => {
expect(launchResult).to.have.property('code', 0);
expect(launchResult.out).to.include('Update Downloaded');
});
await relaunchPromise;
expect(requests[requests.length - 1].url).to.equal('/update-check/updated/4.0.0');
expect(dirsPerDownload).to.have.lengthOf(3);
// 1st: fresh cache, 1 dir.
expect(dirsPerDownload[0]).to.have.lengthOf(1,
`1st download: ${dirsPerDownload[0].join(', ')}`);
// 2nd: staged (1st) preserved + new = 2 dirs.
expect(dirsPerDownload[1]).to.have.lengthOf(2,
`2nd download: ${dirsPerDownload[1].join(', ')}`);
expect(dirsPerDownload[1]).to.include(dirsPerDownload[0][0]);
// 3rd: 1st is now orphaned (plist points to 2nd) — must be pruned.
// Staged (2nd) preserved + new = still 2 dirs. Bounded.
expect(dirsPerDownload[2]).to.have.lengthOf(2,
`3rd download: ${dirsPerDownload[2].join(', ')}`);
expect(dirsPerDownload[2]).to.not.include(dirsPerDownload[0][0],
'The first (now orphaned) update directory must be pruned on the third check');
const secondDir = dirsPerDownload[1].find(d => d !== dirsPerDownload[0][0]);
expect(dirsPerDownload[2]).to.include(secondDir,
'The second (currently staged) update directory must be preserved on the third check');
});
});
});
});
// Regression test for https://github.com/electron/electron/issues/50200
//
// When checkForUpdates() is called again after an update has been staged,
// Squirrel creates a new temporary directory and prunes old ones. If the
// prune removes the directory that ShipItState.plist references while the
// second download is still in flight, a subsequent quitAndInstall() will
// fail with ENOENT and the app will never relaunch.
it('should install the staged update when quitAndInstall is called while a second check is in flight', async () => {
await cleanSquirrelCache();
await withUpdatableApp({
nextVersion: '2.0.0',
startFixture: 'update-race',
endFixture: 'update-race'
}, async (appPath, updateZipPath) => {
let downloadCount = 0;
let stalledResponse: express.Response | null = null;
server.get('/update-file', (req, res) => {
downloadCount++;
if (downloadCount === 1) {
// First download completes normally and stages the update.
res.download(updateZipPath);
} else {
// Second download: stall indefinitely to simulate a slow
// network. This keeps the second check "in progress" when
// quitAndInstall() fires. Hold onto the response so we can
// clean it up later.
stalledResponse = res;
}
});
server.get('/update-check', (req, res) => {
res.json({
url: `http://localhost:${port}/update-file`,
name: 'My Release Name',
notes: 'Theses are some release notes innit',
pub_date: (new Date()).toString()
});
});
const relaunchPromise = new Promise<void>((resolve) => {
server.get('/update-check/updated/:version', (req, res) => {
res.status(204).send();
resolve();
});
});
const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
logOnError(launchResult, () => {
expect(launchResult).to.have.property('code', 0);
expect(launchResult.out).to.include('Update Downloaded');
expect(launchResult.out).to.include('Calling quitAndInstall mid-download');
// First check + first download + second check + stalled second download.
expect(requests).to.have.lengthOf(4);
expect(requests[0]).to.have.property('url', '/update-check');
expect(requests[1]).to.have.property('url', '/update-file');
expect(requests[2]).to.have.property('url', '/update-check');
expect(requests[3]).to.have.property('url', '/update-file');
// The second download must have been in flight (never completed)
// when quitAndInstall was called.
expect(launchResult.out).to.not.include('Unexpected second download completion');
});
// Unblock the stalled response now that the initial app has exited
// so the express server can shut down cleanly.
if (stalledResponse) {
(stalledResponse as express.Response).status(500).end();
}
// The originally staged update (2.0.0) must have been applied and
// the app must relaunch, proving the staged update directory was
// not pruned out from under ShipItState.plist.
await relaunchPromise;
expect(requests).to.have.lengthOf(5);
expect(requests[4].url).to.equal('/update-check/updated/2.0.0');
expect(requests[4].header('user-agent')).to.include('Electron/');
});
});
it('should update to lower version numbers', async () => {
await withUpdatableApp({
nextVersion: '0.0.1',

View File

@@ -5550,7 +5550,7 @@ describe('BrowserWindow module', () => {
thickFrame: true,
transparent: true
});
expect(w.isResizable()).to.be.true('resizable');
expect(w.isResizable()).to.be.false('resizable');
w.maximize();
expect(w.isMaximized()).to.be.true('maximized');
const bounds = w.getBounds();

View File

@@ -129,22 +129,6 @@ describe('utilityProcess module', () => {
expect(code).to.equal(exitCode);
});
ifit(process.platform === 'win32')('emits correct exit code when high bit is set on Windows', async () => {
// NTSTATUS code with high bit set should not be mangled by sign extension.
const exitCode = 0xC0000005;
const child = utilityProcess.fork(path.join(fixturesPath, 'custom-exit.js'), [`--exitCode=${exitCode}`]);
const [code] = await once(child, 'exit');
expect(code).to.equal(exitCode);
});
ifit(process.platform !== 'win32')('emits correct exit code when child process crashes on posix', async () => {
// Crash exit codes should not be sign-extended to large 64-bit values.
const child = utilityProcess.fork(path.join(fixturesPath, 'crash.js'));
const [code] = await once(child, 'exit');
expect(code).to.not.equal(0);
expect(code).to.be.lessThanOrEqual(0xFFFFFFFF);
});
it('does not run JS after process.exit is called', async () => {
const file = path.join(os.tmpdir(), `no-js-after-exit-log-${Math.random()}`);
const child = utilityProcess.fork(path.join(fixturesPath, 'no-js-after-exit.js'), [`--testPath=${file}`]);

View File

@@ -1666,18 +1666,6 @@ describe('webContents module', () => {
});
});
describe('getOrCreateDevToolsTargetId()', () => {
afterEach(closeAllWindows);
it('returns the devtools target id', async () => {
const w = new BrowserWindow({ show: false });
await w.loadURL('about:blank');
const devToolsId = w.webContents.getOrCreateDevToolsTargetId();
expect(devToolsId).to.be.a('string').that.is.not.empty();
// Verify it's the inverse of fromDevToolsTargetId
expect(webContents.fromDevToolsTargetId(devToolsId)).to.equal(w.webContents);
});
});
describe('userAgent APIs', () => {
afterEach(closeAllWindows);
it('is not empty by default', () => {

View File

@@ -407,41 +407,6 @@ describe('asar package', function () {
});
});
describe('fs.cpSync', function () {
itremote('copies a normal file', function () {
if (!fs.cpSync) return;
const p = path.join(asarDir, 'a.asar', 'file1');
const temp = require('temp').track();
const dest = temp.path();
fs.cpSync(p, dest);
expect(fs.readFileSync(p).equals(fs.readFileSync(dest))).to.be.true();
});
});
describe('fs.cp', function () {
itremote('copies a normal file', async function () {
if (!fs.cp) return;
const p = path.join(asarDir, 'a.asar', 'file1');
const temp = require('temp').track();
const dest = temp.path();
await new Promise<void>((resolve, reject) => {
fs.cp(p, dest, (err) => err ? reject(err) : resolve());
});
expect(fs.readFileSync(p).equals(fs.readFileSync(dest))).to.be.true();
});
});
describe('fs.promises.cp', function () {
itremote('copies a normal file', async function () {
if (!fs.promises.cp) return;
const p = path.join(asarDir, 'a.asar', 'file1');
const temp = require('temp').track();
const dest = temp.path();
await fs.promises.cp(p, dest);
expect(fs.readFileSync(p).equals(fs.readFileSync(dest))).to.be.true();
});
});
describe('fs.lstatSync', function () {
itremote('handles path with trailing slash correctly', function () {
const p = path.join(asarDir, 'a.asar', 'link2', 'link2', 'file1');

View File

@@ -1438,89 +1438,6 @@ describe('chromium features', () => {
expect(data).to.equal('object function object function');
});
it('Worker does not have node integration when nodeIntegrationInWorker is disabled via setWindowOpenHandler', async () => {
const w = new BrowserWindow({
show: false,
webPreferences: {
nodeIntegration: true,
nodeIntegrationInWorker: true,
contextIsolation: false
}
});
w.webContents.setWindowOpenHandler(() => ({
action: 'allow',
overrideBrowserWindowOptions: {
show: false,
webPreferences: {
nodeIntegration: false,
nodeIntegrationInWorker: false,
contextIsolation: true
}
}
}));
await w.loadURL(`file://${fixturesPath}/pages/blank.html`);
const childCreated = once(app, 'browser-window-created') as Promise<[any, BrowserWindow]>;
w.webContents.executeJavaScript(`window.open(${JSON.stringify(`file://${fixturesPath}/pages/blank.html`)}); void 0;`);
const [, child] = await childCreated;
await once(child.webContents, 'did-finish-load');
const data = await child.webContents.executeJavaScript(`
const worker = new Worker('../workers/worker_node.js');
new Promise((resolve) => { worker.onmessage = e => resolve(e.data); })
`);
expect(data).to.equal('undefined undefined undefined undefined');
});
it('Worker has node integration when nodeIntegrationInWorker is enabled via setWindowOpenHandler', async () => {
const w = new BrowserWindow({
show: false,
webPreferences: {
nodeIntegration: true,
nodeIntegrationInWorker: false,
contextIsolation: false
}
});
w.webContents.setWindowOpenHandler(() => ({
action: 'allow',
overrideBrowserWindowOptions: {
show: false,
webPreferences: {
nodeIntegration: true,
nodeIntegrationInWorker: true,
contextIsolation: false
}
}
}));
await w.loadURL(`file://${fixturesPath}/pages/blank.html`);
// Parent's workers should NOT have node integration.
const parentData = await w.webContents.executeJavaScript(`
new Promise((resolve) => {
const worker = new Worker('../workers/worker_node.js');
worker.onmessage = e => resolve(e.data);
})
`);
expect(parentData).to.equal('undefined undefined undefined undefined');
const childCreated = once(app, 'browser-window-created') as Promise<[any, BrowserWindow]>;
w.webContents.executeJavaScript(`window.open(${JSON.stringify(`file://${fixturesPath}/pages/blank.html`)}); void 0;`);
const [, child] = await childCreated;
await once(child.webContents, 'did-finish-load');
// Child's workers should have node integration.
const childData = await child.webContents.executeJavaScript(`
new Promise((resolve) => {
const worker = new Worker('../workers/worker_node.js');
worker.onmessage = e => resolve(e.data);
})
`);
expect(childData).to.equal('object function object function');
});
it('Worker has access to fetch-dependent interfaces with nodeIntegrationInWorker', async () => {
const w = new BrowserWindow({
show: false,

View File

@@ -1,82 +0,0 @@
const { app, autoUpdater } = require('electron');
const fs = require('node:fs');
const path = require('node:path');
process.on('uncaughtException', (err) => {
console.error(err);
process.exit(1);
});
let installInvoked = false;
autoUpdater.on('error', (err) => {
// Once quitAndInstall() has been invoked the second in-flight check may
// surface a cancellation/network error as the process tears down; ignore
// errors after that point so we test the actual install race, not teardown.
if (installInvoked) {
console.log('Ignoring post-install error:', err && err.message);
return;
}
console.error(err);
process.exit(1);
});
const urlPath = path.resolve(__dirname, '../../../../url.txt');
let feedUrl = process.argv[1];
if (feedUrl === 'remain-open') {
// Hold the event loop
setInterval(() => {});
} else {
if (!feedUrl || !feedUrl.startsWith('http')) {
feedUrl = `${fs.readFileSync(urlPath, 'utf8')}/${app.getVersion()}`;
} else {
fs.writeFileSync(urlPath, `${feedUrl}/updated`);
}
autoUpdater.setFeedURL({
url: feedUrl
});
autoUpdater.checkForUpdates();
autoUpdater.on('update-available', () => {
console.log('Update Available');
});
let downloadedOnce = false;
autoUpdater.on('update-downloaded', () => {
console.log('Update Downloaded');
if (!downloadedOnce) {
downloadedOnce = true;
// Simulate a periodic update check firing after an update was already
// staged. The test server is expected to stall this second download so
// that it remains in flight while we call quitAndInstall().
// The short delay lets checkForUpdatesCommand's RACCommand executing
// state settle; calling immediately would hit the command's "disabled"
// guard since RACCommand disallows concurrent execution.
setTimeout(() => {
autoUpdater.checkForUpdates();
// Give Squirrel enough time to enter the second check (creating a new
// temporary directory, which with the regression prunes the directory
// that the staged update lives in) before invoking the install.
setTimeout(() => {
console.log('Calling quitAndInstall mid-download');
installInvoked = true;
autoUpdater.quitAndInstall();
}, 3000);
}, 1000);
} else {
// Should not reach here — the second download is stalled on purpose.
console.log('Unexpected second download completion');
autoUpdater.quitAndInstall();
}
});
autoUpdater.on('update-not-available', () => {
console.error('No update available');
process.exit(1);
});
}

View File

@@ -1,5 +0,0 @@
{
"name": "electron-test-update-race",
"version": "1.0.0",
"main": "./index.js"
}

View File

@@ -1,57 +0,0 @@
const { app, autoUpdater } = require('electron');
const fs = require('node:fs');
const path = require('node:path');
process.on('uncaughtException', (err) => {
console.error(err);
process.exit(1);
});
autoUpdater.on('error', (err) => {
console.error(err);
process.exit(1);
});
const urlPath = path.resolve(__dirname, '../../../../url.txt');
let feedUrl = process.argv[1];
if (feedUrl === 'remain-open') {
// Hold the event loop
setInterval(() => {});
} else {
if (!feedUrl || !feedUrl.startsWith('http')) {
feedUrl = `${fs.readFileSync(urlPath, 'utf8')}/${app.getVersion()}`;
} else {
fs.writeFileSync(urlPath, `${feedUrl}/updated`);
}
autoUpdater.setFeedURL({
url: feedUrl
});
autoUpdater.checkForUpdates();
autoUpdater.on('update-available', () => {
console.log('Update Available');
});
let updateStackCount = 0;
autoUpdater.on('update-downloaded', () => {
updateStackCount++;
console.log('Update Downloaded');
if (updateStackCount > 2) {
autoUpdater.quitAndInstall();
} else {
setTimeout(() => {
autoUpdater.checkForUpdates();
}, 1000);
}
});
autoUpdater.on('update-not-available', () => {
console.error('No update available');
process.exit(1);
});
}

View File

@@ -1,5 +0,0 @@
{
"name": "electron-test-update-triple-stack",
"version": "1.0.0",
"main": "./index.js"
}

View File

@@ -1,7 +0,0 @@
<html>
<body>
<script>
window.open('javascript:alert()');
</script>
</body>
</html>

View File

@@ -1,22 +0,0 @@
const { app, BrowserWindow } = require('electron');
process.on('uncaughtException', (err) => {
console.error(err);
process.exit(1);
});
process.on('unhandledRejection', (reason) => {
console.error(reason);
process.exit(1);
});
app.on('browser-window-created', (_, window) => {
window.webContents.once('did-frame-navigate', () => {
process.exit(0);
});
});
app.whenReady().then(() => {
const win = new BrowserWindow({ show: false });
win.loadFile('index.html');
});

View File

@@ -9,7 +9,7 @@
}
}
const parsedURL = new URL(window.location.href);
if (parsedURL.searchParams.has('opened')) {
if (parsedURL.searchParams.get('opened') != null) {
// Ensure origins are properly checked by removing a single character from the end
tryPostMessage('do not deliver substring origin', window.location.origin.substring(0, window.location.origin.length - 1))
tryPostMessage('do not deliver file://', 'file://')