Compare commits

..

7 Commits

Author SHA1 Message Date
Keeley Hammond
26437c6202 fix: discover ThinLTO cache path from GN instead of hardcoding
Addresses review feedback from @deepak1556: the hardcoded
`out\Default\thinlto-cache` path goes out of sync if upstream
changes `cache_dir` in Chromium's build/config/compiler/BUILD.gn.

Read the `/lldltocache:` flag from `gn desc` on a linked target
(`//electron:electron_app`) and pre-create whatever path GN
actually configured. Skips the pre-create entirely when ThinLTO
is disabled (non-official builds), which is the correct no-op.
2026-04-23 12:12:31 -07:00
Keeley Hammond
f038413cf9 fix: pre-create thinlto-cache dir on Windows to avoid bindflt race
Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>
2026-04-23 10:16:38 -07:00
Samuel Attard
099c5c0038 build: use 32-core Windows ARC runners for build jobs (#51256)
* build: use 32-core Windows ARC runners for build jobs

* ci: add siso patch to retry ERROR_INVALID_PARAMETER on ninja file open

The existing patch removes the redundant per-chunk re-open in
fileParser.readFile, but the single remaining os.Open per subninja can
still hit the bindflt race under the ~90k-file concurrent open burst on
the 32-core Windows runners. Layer a second patch on top that wraps that
open in a Windows-only 5-attempt retry (5-80ms backoff) so a single
transient failure no longer aborts the whole manifest load.
2026-04-23 02:23:32 -07:00
Charles Kerr
2c46abe361 test: add linux coverage for default protocol client APIs (#51253)
Add Linux-only app tests to check the default protocol handler.
This includes adding reusable XDG mock fixtures.
2026-04-23 10:10:08 +02:00
David Sanders
05e0cd085c build: drop script/run-gn-format.py (#51263) 2026-04-23 09:52:36 +02:00
Asish Kumar
7c56577639 fix: preserve return value in deprecate.removeFunction (#51028)
The wrapper returned by `deprecate.removeFunction` dropped the wrapped
function's return value because it did not `return` from `fn.apply`.
Every other function wrapper in this module (`renameFunction`,
`moveAPI`) forwards the return value, and the generic type signature
`<T extends Function>(fn: T, ...): T` promises that `T`'s return type
is preserved. Callers that relied on the return value of a function
wrapped by `removeFunction` would silently receive `undefined` from
the wrapper.

Mirror the forwarding done by `renameFunction` / `moveAPI` and extend
the existing spec to assert that the wrapper preserves both the return
value and the `this` context of the deprecated function.

Assisted-By: Claude Opus 4.6

Signed-off-by: Asish Kumar <officialasishkumar@gmail.com>
2026-04-23 00:05:30 +00:00
Robo
350de668e2 refactor: api::autoUpdater managed by cppgc (#51241) 2026-04-23 05:38:47 +09:00
19 changed files with 460 additions and 200 deletions

View File

@@ -101,6 +101,21 @@ runs:
git pack-refs
cd ..
# Pre-create the ThinLTO cache directory so lld-link does not need to
# call CreateDirectoryW through the bindflt filter driver, which can
# return ERROR_INVALID_PARAMETER under concurrent I/O on ARC runners.
# Discover the path from GN instead of hardcoding it so we stay in
# sync with `cache_dir` in build/config/compiler/BUILD.gn; skip the
# pre-create when ThinLTO is disabled (non-official builds).
$env:ELECTRON_DEPOT_TOOLS_DISABLE_LOG = "1"
$ltoFlag = e d gn desc out/Default //electron:electron_app ldflags 2>$null |
Select-String -Pattern '^/lldltocache:(.+)$' |
Select-Object -First 1
if ($ltoFlag) {
$cachePath = Join-Path 'out\Default' $ltoFlag.Matches[0].Groups[1].Value
New-Item -ItemType Directory -Force -Path $cachePath | Out-Null
}
$env:NINJA_SUMMARIZE_BUILD = 1
if ("${{ inputs.is-release }}" -eq "true") {
e build --target electron:release_build

View File

@@ -0,0 +1,132 @@
From a8afee1089ec2ae9ab5837b438d07338aefb3bc4 Mon Sep 17 00:00:00 2001
From: Samuel Attard <sam@electronjs.org>
Date: Wed, 22 Apr 2026 16:27:51 -0700
Subject: [PATCH] siso: retry transient ERROR_INVALID_PARAMETER when opening
ninja files on Windows
ManifestParser.Load fans out across all subninja files (~90k in a
Chromium build) at NumCPU parallelism. On Windows builders where out/
is served through a filesystem filter driver (e.g. bindflt/wcifs for
container bind mounts), CreateFileW can intermittently return
ERROR_INVALID_PARAMETER under this concurrent open burst. The previous
patch removes the redundant per-chunk re-open, but the single remaining
open per file can still hit the race; without a retry a single transient
failure aborts the entire manifest load.
Wrap the remaining os.Open call in readFile in a small Windows-only
retry for ERROR_INVALID_PARAMETER (5 attempts, 5-80ms backoff). Each
retry is logged via clog.Warningf and also written to stderr so it is
visible in CI step output where glog warnings are file-only by default.
Other platforms keep the direct os.Open path.
---
siso/toolsupport/ninjautil/file_parser.go | 3 +-
siso/toolsupport/ninjautil/openfile_other.go | 18 +++++++
.../toolsupport/ninjautil/openfile_windows.go | 50 +++++++++++++++++++
3 files changed, 69 insertions(+), 2 deletions(-)
create mode 100644 siso/toolsupport/ninjautil/openfile_other.go
create mode 100644 siso/toolsupport/ninjautil/openfile_windows.go
diff --git a/siso/toolsupport/ninjautil/file_parser.go b/siso/toolsupport/ninjautil/file_parser.go
index 6311666..324528d 100644
--- a/siso/toolsupport/ninjautil/file_parser.go
+++ b/siso/toolsupport/ninjautil/file_parser.go
@@ -7,7 +7,6 @@ package ninjautil
import (
"context"
"fmt"
- "os"
"runtime/trace"
"sync"
"time"
@@ -91,7 +90,7 @@ func (p *fileParser) parseFile(ctx context.Context, fname string) error {
// readFile reads a file of fname in parallel.
func (p *fileParser) readFile(ctx context.Context, fname string) ([]byte, error) {
defer trace.StartRegion(ctx, "ninja.read").End()
- f, err := os.Open(fname)
+ f, err := openFile(ctx, fname)
if err != nil {
return nil, err
}
diff --git a/siso/toolsupport/ninjautil/openfile_other.go b/siso/toolsupport/ninjautil/openfile_other.go
new file mode 100644
index 0000000..9fca690
--- /dev/null
+++ b/siso/toolsupport/ninjautil/openfile_other.go
@@ -0,0 +1,18 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+//go:build !windows
+
+package ninjautil
+
+import (
+ "context"
+ "os"
+)
+
+// openFile opens fname for reading.
+// See openfile_windows.go for the Windows variant with transient-error retry.
+func openFile(ctx context.Context, fname string) (*os.File, error) {
+ return os.Open(fname)
+}
diff --git a/siso/toolsupport/ninjautil/openfile_windows.go b/siso/toolsupport/ninjautil/openfile_windows.go
new file mode 100644
index 0000000..f9d8e9d
--- /dev/null
+++ b/siso/toolsupport/ninjautil/openfile_windows.go
@@ -0,0 +1,50 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+//go:build windows
+
+package ninjautil
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "os"
+ "time"
+
+ "golang.org/x/sys/windows"
+
+ "go.chromium.org/build/siso/o11y/clog"
+)
+
+// openFile opens fname for reading, retrying transient
+// ERROR_INVALID_PARAMETER failures.
+//
+// On Windows, CreateFileW can intermittently return
+// ERROR_INVALID_PARAMETER when the target lives behind a filesystem
+// filter driver (e.g. bindflt/wcifs for container bind mounts) under
+// highly concurrent opens. loadFile fans out across ~90k subninja
+// files at NumCPU parallelism, so a single transient failure would
+// otherwise abort the whole manifest load.
+func openFile(ctx context.Context, fname string) (*os.File, error) {
+ const maxAttempts = 5
+ delay := 5 * time.Millisecond
+ for i := 0; ; i++ {
+ f, err := os.Open(fname)
+ if err == nil {
+ return f, nil
+ }
+ if i+1 >= maxAttempts || !errors.Is(err, windows.ERROR_INVALID_PARAMETER) {
+ return nil, err
+ }
+ clog.Warningf(ctx, "open %s: %v; retrying (%d/%d) after %s", fname, err, i+1, maxAttempts, delay)
+ fmt.Fprintf(os.Stderr, "siso: open %s: %v; retrying (%d/%d) after %s\n", fname, err, i+1, maxAttempts, delay)
+ select {
+ case <-time.After(delay):
+ case <-ctx.Done():
+ return nil, context.Cause(ctx)
+ }
+ delay *= 2
+ }
+}
--
2.53.0

View File

@@ -398,7 +398,7 @@ jobs:
needs: [checkout-windows, build-siso-windows]
if: ${{ needs.setup.outputs.src == 'true' && !inputs.skip-windows }}
with:
build-runs-on: electron-arc-centralus-windows-amd64-16core
build-runs-on: electron-arc-centralus-windows-amd64-32core
clang-tidy-runs-on: electron-arc-centralus-linux-amd64-8core
test-runs-on: windows-latest
clang-tidy-container: '{"image":"ghcr.io/electron/build:${{ needs.checkout-windows.outputs.build-image-sha }}","options":"--user root --device /dev/fuse --cap-add SYS_ADMIN","volumes":["/mnt/win-cache:/mnt/win-cache"]}'
@@ -419,7 +419,7 @@ jobs:
needs: [checkout-windows, build-siso-windows]
if: ${{ needs.setup.outputs.src == 'true' && !inputs.skip-windows }}
with:
build-runs-on: electron-arc-centralus-windows-amd64-16core
build-runs-on: electron-arc-centralus-windows-amd64-32core
test-runs-on: windows-latest
target-platform: win
target-arch: x86
@@ -438,7 +438,7 @@ jobs:
needs: [checkout-windows, build-siso-windows]
if: ${{ needs.setup.outputs.src == 'true' && !inputs.skip-windows }}
with:
build-runs-on: electron-arc-centralus-windows-amd64-16core
build-runs-on: electron-arc-centralus-windows-amd64-32core
test-runs-on: windows-11-arm
target-platform: win
target-arch: arm64

View File

@@ -82,7 +82,7 @@ jobs:
needs: [checkout-windows, build-siso-windows]
with:
environment: production-release
build-runs-on: electron-arc-centralus-windows-amd64-16core
build-runs-on: electron-arc-centralus-windows-amd64-32core
target-platform: win
target-arch: x64
is-release: true
@@ -101,7 +101,7 @@ jobs:
needs: [checkout-windows, build-siso-windows]
with:
environment: production-release
build-runs-on: electron-arc-centralus-windows-amd64-16core
build-runs-on: electron-arc-centralus-windows-amd64-32core
target-platform: win
target-arch: arm64
is-release: true
@@ -120,7 +120,7 @@ jobs:
needs: [checkout-windows, build-siso-windows]
with:
environment: production-release
build-runs-on: electron-arc-centralus-windows-amd64-16core
build-runs-on: electron-arc-centralus-windows-amd64-32core
target-platform: win
target-arch: x86
is-release: true

View File

@@ -56,7 +56,7 @@ export function removeFunction<T extends Function>(fn: T, removedName: string):
const warn = warnOnce(`${fn.name} function`);
return function (this: any) {
warn();
fn.apply(this, arguments);
return fn.apply(this, arguments);
} as unknown as typeof fn;
}

View File

@@ -78,7 +78,7 @@
"gn-typescript-definitions": "npm run create-typescript-definitions && node script/cp.mjs electron.d.ts",
"pre-flight": "pre-flight",
"gn-check": "node ./script/gn-check.js",
"gn-format": "python3 script/run-gn-format.py",
"gn-format": "node ./script/lint.js --gn --fix",
"precommit": "lint-staged",
"preinstall": "node -e 'process.exit(0)'",
"pretest": "npm run create-typescript-definitions",
@@ -117,7 +117,7 @@
],
"*.{gn,gni}": [
"npm run gn-check",
"npm run gn-format"
"node ./script/lint.js --gn --fix --only --"
],
"*.py": [
"node script/lint.js --py --fix --only --"

View File

@@ -1,96 +1,44 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: deepak1556 <hop2deep@gmail.com>
Date: Tue, 7 Apr 2026 02:34:49 +0900
From: Samuel Attard <sattard@salesforce.com>
Date: Thu, 26 May 2022 15:38:32 -0700
Subject: feat: filter out non-shareable windows in the current application in
ScreenCaptureKitDevice
Ensures that windows with sharingType == NSWindowSharingNone (set via
Electron's win.setContentProtection(true)) are excluded from full-display
ScreenCaptureKit captures. The filtering is extracted into a
GetNonShareableWindows() helper and applied in both OnShareableContentCreated
(initial stream setup) and OnShareableContentForFilterUpdate (PiP state
change handler), so content-protected windows stay excluded even when the
SCContentFilter is rebuilt during capture.
Limitation: only filters windows protected before the query runs. Dynamically
protecting a window during an active capture will not take effect until the
next filter update or stream restart.
This patch ensures that windows protected via win.setContentProtection(true) do not appear in full display captures via desktopCapturer. This patch could be upstreamed but as the check is limited to in-process windows it doesn't make a lot of sense for Chromium itself. This patch currently has a limitation that it only function for windows created / protected BEFORE the stream is started. There is theoretical future work we can do via polling / observers to automatically update the SCContentFilter when new windows are made but for now this will solve 99+% of the problem and folks can re-order their logic a bit to get it working for their use cases.
diff --git a/content/browser/media/capture/screen_capture_kit_device_mac.mm b/content/browser/media/capture/screen_capture_kit_device_mac.mm
index 64ffc2642c003c8fb7f133ee43ba3e20d48ea543..e101c1c3a09074ecefb6ad59327f7c518f4780e4 100644
index f7523cf4dcc40b13499d1e425ce7b67f2d907cc9..64ffc2642c003c8fb7f133ee43ba3e20d48ea543 100644
--- a/content/browser/media/capture/screen_capture_kit_device_mac.mm
+++ b/content/browser/media/capture/screen_capture_kit_device_mac.mm
@@ -154,6 +154,32 @@ bool IsPresenterOverlayLargeActive(CFDictionaryRef attachment) {
}
return @[];
}
+
+// Returns SCWindows from |content| that correspond to in-process NSWindows
+// whose sharingType is NSWindowSharingNone (content-protected via Electron's
+// win.setContentProtection(true)). Only captures windows protected before this
+// call; dynamically protected windows require a filter update.
+API_AVAILABLE(macos(12.3))
+NSArray<SCWindow*>* GetNonShareableWindows(SCShareableContent* content) {
+ NSArray<NSWindow*>* non_sharing_nswindows = [[[NSApplication sharedApplication]
+ windows]
+ filteredArrayUsingPredicate:
+ [NSPredicate predicateWithBlock:^BOOL(NSWindow* win,
+ NSDictionary* bindings) {
+ return [win sharingType] == NSWindowSharingNone;
+ }]];
+ return [[content windows]
+ filteredArrayUsingPredicate:
+ [NSPredicate predicateWithBlock:^BOOL(SCWindow* win,
+ NSDictionary* bindings) {
+ for (NSWindow* excluded : non_sharing_nswindows) {
+ if ((CGWindowID)[excluded windowNumber] == [win windowID]) {
+ return true;
+ }
+ }
+ return false;
+ }]];
+}
} // namespace
API_AVAILABLE(macos(12.3))
@@ -322,30 +348,8 @@ void OnShareableContentCreated(SCShareableContent* content) {
@@ -322,6 +322,31 @@ void OnShareableContentCreated(SCShareableContent* content) {
source_.id == webrtc::kFullDesktopScreenId) {
NSArray<SCWindow*>* excluded_windows = GetWindowsToExclude(
content, pip_screen_capture_coordinator_proxy_.get(), source_);
- NSArray<NSWindow*>* non_sharing_nswindows = [[[NSApplication
- sharedApplication] windows]
- filteredArrayUsingPredicate:[NSPredicate
- predicateWithBlock:^BOOL(
- NSWindow* win,
- NSDictionary* bindings) {
- return [win sharingType] ==
- NSWindowSharingNone;
- }]];
- NSArray<SCWindow*>* non_sharing_scwindows = [[content windows]
- filteredArrayUsingPredicate:
- [NSPredicate predicateWithBlock:^BOOL(
- SCWindow* win, NSDictionary* bindings) {
- for (NSWindow* excluded : non_sharing_nswindows) {
- if ((CGWindowID)[excluded windowNumber] ==
- [win windowID]) {
- return true;
- }
- }
- return false;
- }]];
- // Combine excluded windows from PiP and non-shareable windows.
excluded_windows = [excluded_windows
- arrayByAddingObjectsFromArray:non_sharing_scwindows];
+ arrayByAddingObjectsFromArray:GetNonShareableWindows(content)];
+ NSArray<NSWindow*>* non_sharing_nswindows = [[[NSApplication
+ sharedApplication] windows]
+ filteredArrayUsingPredicate:[NSPredicate
+ predicateWithBlock:^BOOL(
+ NSWindow* win,
+ NSDictionary* bindings) {
+ return [win sharingType] ==
+ NSWindowSharingNone;
+ }]];
+ NSArray<SCWindow*>* non_sharing_scwindows = [[content windows]
+ filteredArrayUsingPredicate:
+ [NSPredicate predicateWithBlock:^BOOL(
+ SCWindow* win, NSDictionary* bindings) {
+ for (NSWindow* excluded : non_sharing_nswindows) {
+ if ((CGWindowID)[excluded windowNumber] ==
+ [win windowID]) {
+ return true;
+ }
+ }
+ return false;
+ }]];
+ // Combine excluded windows from PiP and non-shareable windows.
+ excluded_windows = [excluded_windows
+ arrayByAddingObjectsFromArray:non_sharing_scwindows];
+
filter = [[SCContentFilter alloc] initWithDisplay:display
excludingWindows:excluded_windows];
@@ -614,6 +618,8 @@ void OnShareableContentForFilterUpdate(SCShareableContent* content) {
NSArray<SCWindow*>* excluded_windows = GetWindowsToExclude(
content, pip_screen_capture_coordinator_proxy_.get(), source_);
+ excluded_windows = [excluded_windows
+ arrayByAddingObjectsFromArray:GetNonShareableWindows(content)];
SCContentFilter* filter =
[[SCContentFilter alloc] initWithDisplay:display
excludingWindows:excluded_windows];
stream_config_content_size_ =

View File

@@ -1,25 +0,0 @@
import os
import subprocess
import sys
from lib.util import get_depot_tools_env
SOURCE_ROOT = os.path.dirname(os.path.dirname(__file__))
# Helper to run gn format on multiple files
# (gn only formats a single file at a time)
def main():
new_env = get_depot_tools_env()
new_env['DEPOT_TOOLS_WIN_TOOLCHAIN'] = '0'
new_env['CHROMIUM_BUILDTOOLS_PATH'] = os.path.realpath(
os.path.join(SOURCE_ROOT, '..', 'buildtools')
)
for gn_file in sys.argv[1:]:
subprocess.check_call(
['gn', 'format', gn_file],
env=new_env
)
if __name__ == '__main__':
sys.exit(main())

View File

@@ -12,20 +12,28 @@
#include "shell/common/gin_converters/time_converter.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/event_emitter_caller.h"
#include "shell/common/gin_helper/handle.h"
#include "shell/common/gin_helper/object_template_builder.h"
#include "shell/common/gin_helper/wrappable_pointer_tags.h"
#include "shell/common/node_includes.h"
#include "v8/include/cppgc/allocation.h"
#include "v8/include/v8-cppgc.h"
namespace electron::api {
gin::DeprecatedWrapperInfo AutoUpdater::kWrapperInfo = {
gin::kEmbedderNativeGin};
const gin::WrapperInfo AutoUpdater::kWrapperInfo =
electron::MakeWrapperInfo(electron::kElectronAutoUpdater);
AutoUpdater::AutoUpdater() {
AutoUpdater::AutoUpdater(v8::Isolate* isolate) {
auto_updater::AutoUpdater::SetDelegate(this);
gin::PerIsolateData* data = gin::PerIsolateData::From(isolate);
data->AddDisposeObserver(this);
}
AutoUpdater::~AutoUpdater() {
AutoUpdater::~AutoUpdater() = default;
void AutoUpdater::OnBeforeMicrotasksRunnerDispose(v8::Isolate* isolate) {
gin::PerIsolateData* data = gin::PerIsolateData::From(isolate);
data->RemoveDisposeObserver(this);
auto_updater::AutoUpdater::SetDelegate(nullptr);
}
@@ -120,8 +128,9 @@ void AutoUpdater::QuitAndInstall() {
}
// static
gin_helper::Handle<AutoUpdater> AutoUpdater::Create(v8::Isolate* isolate) {
return gin_helper::CreateHandle(isolate, new AutoUpdater());
AutoUpdater* AutoUpdater::Create(v8::Isolate* isolate) {
return cppgc::MakeGarbageCollected<AutoUpdater>(
isolate->GetCppHeap()->GetAllocationHandle(), isolate);
}
gin::ObjectTemplateBuilder AutoUpdater::GetObjectTemplateBuilder(
@@ -138,8 +147,12 @@ gin::ObjectTemplateBuilder AutoUpdater::GetObjectTemplateBuilder(
.SetMethod("quitAndInstall", &AutoUpdater::QuitAndInstall);
}
const char* AutoUpdater::GetTypeName() {
return "AutoUpdater";
const gin::WrapperInfo* AutoUpdater::wrapper_info() const {
return &kWrapperInfo;
}
const char* AutoUpdater::GetHumanReadableName() const {
return "Electron / AutoUpdater";
}
} // namespace electron::api

View File

@@ -7,39 +7,44 @@
#include <string>
#include "gin/per_isolate_data.h"
#include "gin/wrappable.h"
#include "shell/browser/auto_updater.h"
#include "shell/browser/event_emitter_mixin.h"
#include "shell/browser/window_list_observer.h"
#include "shell/common/gin_helper/wrappable.h"
namespace gin_helper {
template <typename T>
class Handle;
} // namespace gin_helper
namespace electron::api {
class AutoUpdater final : public gin_helper::DeprecatedWrappable<AutoUpdater>,
class AutoUpdater final : public gin::Wrappable<AutoUpdater>,
public gin_helper::EventEmitterMixin<AutoUpdater>,
public auto_updater::Delegate,
public gin::PerIsolateData::DisposeObserver,
private WindowListObserver {
public:
static gin_helper::Handle<AutoUpdater> Create(v8::Isolate* isolate);
static AutoUpdater* Create(v8::Isolate* isolate);
// gin_helper::Wrappable
static gin::DeprecatedWrapperInfo kWrapperInfo;
// gin::Wrappable
static const gin::WrapperInfo kWrapperInfo;
const gin::WrapperInfo* wrapper_info() const override;
const char* GetHumanReadableName() const override;
gin::ObjectTemplateBuilder GetObjectTemplateBuilder(
v8::Isolate* isolate) override;
const char* GetTypeName() override;
const char* GetClassName() const { return "AutoUpdater"; }
// gin::PerIsolateData::DisposeObserver
void OnBeforeDispose(v8::Isolate* isolate) override {}
void OnBeforeMicrotasksRunnerDispose(v8::Isolate* isolate) override;
void OnDisposed() override {}
// Make public for cppgc::MakeGarbageCollected.
explicit AutoUpdater(v8::Isolate* isolate);
~AutoUpdater() override;
// disable copy
AutoUpdater(const AutoUpdater&) = delete;
AutoUpdater& operator=(const AutoUpdater&) = delete;
protected:
AutoUpdater();
~AutoUpdater() override;
private:
// auto_updater::Delegate:
void OnError(const std::string& message) override;
void OnError(const std::string& message,
@@ -56,7 +61,6 @@ class AutoUpdater final : public gin_helper::DeprecatedWrappable<AutoUpdater>,
// WindowListObserver:
void OnWindowAllClosed() override;
private:
std::string GetFeedURL();
void QuitAndInstall();
};

View File

@@ -13,6 +13,7 @@ namespace electron {
// Electron-specific WrappablePointerTag values that extend gin's tag range.
enum ElectronWrappablePointerTag : uint16_t {
kElectronApp = gin::kLastPointerTag + 1, // electron::api::App
kElectronAutoUpdater, // electron::api::AutoUpdater
kElectronCookies, // electron::api::Cookies
kElectronDataPipeHolder, // electron::api::DataPipeHolder
kElectronDebugger, // electron::api::Debugger

View File

@@ -9,16 +9,32 @@ import * as fs from 'node:fs';
import * as http from 'node:http';
import * as https from 'node:https';
import * as net from 'node:net';
import * as os from 'node:os';
import * as path from 'node:path';
import * as readline from 'node:readline';
import { setTimeout } from 'node:timers/promises';
import { promisify } from 'node:util';
import { collectStreamBody, getResponse } from './lib/net-helpers';
import { ifdescribe, ifit, isWayland, listen, waitUntil } from './lib/spec-helpers';
import { defer, ifdescribe, ifit, isWayland, listen, waitUntil } from './lib/spec-helpers';
import { closeWindow, closeAllWindows } from './lib/window-helpers';
const fixturesPath = path.resolve(__dirname, 'fixtures');
const xdgMockFixturePath = path.join(fixturesPath, 'api', 'xdg-mock');
function makeXdgMockDirectories(prefix: string) {
const xdgDir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
fs.cpSync(xdgMockFixturePath, xdgDir, { recursive: true });
const xdgDataHome = path.join(xdgDir, 'data');
const xdgConfigHome = path.join(xdgDir, 'config');
const xdgBinDir = path.join(xdgDir, 'bin');
fs.chmodSync(path.join(xdgBinDir, 'xdg-mime'), 0o755);
fs.chmodSync(path.join(xdgBinDir, 'xdg-settings'), 0o755);
return { xdgDir, xdgDataHome, xdgConfigHome, xdgBinDir };
}
const isMacOSx64 = process.platform === 'darwin' && process.arch === 'x64';
@@ -1515,7 +1531,6 @@ describe('app module', () => {
const fixtureApp = path.join(fixturesPath, 'api', 'protocol-name');
const desktopFileId = 'mock-browser.desktop';
const mockScheme = 'mockproto';
const mockMimeType = `x-scheme-handler/${mockScheme}`;
function spawnWithXdgMock(
url: string,
@@ -1564,62 +1579,7 @@ describe('app module', () => {
let xdgConfigHome: string;
let xdgBinDir: string;
before(() => {
xdgDir = fs.mkdtempSync(path.join(require('node:os').tmpdir(), 'electron-xdg-'));
xdgDataHome = path.join(xdgDir, 'data');
xdgConfigHome = path.join(xdgDir, 'config');
xdgBinDir = path.join(xdgDir, 'bin');
const appsDir = path.join(xdgDataHome, 'applications');
fs.mkdirSync(appsDir, { recursive: true });
fs.mkdirSync(xdgConfigHome, { recursive: true });
fs.mkdirSync(xdgBinDir, { recursive: true });
fs.writeFileSync(
path.join(appsDir, desktopFileId),
[
'[Desktop Entry]',
'Name=Mock Browser',
'Exec=/usr/bin/true %u',
'Type=Application',
`MimeType=${mockMimeType};`
].join('\n')
);
const mimeAppsContents = [
'[Default Applications]',
`${mockMimeType}=${desktopFileId}`,
'',
'[Added Associations]',
`${mockMimeType}=${desktopFileId};`
].join('\n');
fs.writeFileSync(path.join(xdgConfigHome, 'mimeapps.list'), mimeAppsContents);
fs.writeFileSync(path.join(appsDir, 'mimeapps.list'), mimeAppsContents);
fs.writeFileSync(path.join(appsDir, 'defaults.list'), mimeAppsContents);
// Different xdg-utils versions resolve custom XDG dirs differently, so
// prepend a deterministic xdg-mime shim for this test.
const xdgMimePath = path.join(xdgBinDir, 'xdg-mime');
fs.writeFileSync(
xdgMimePath,
[
'#!/bin/sh',
'if [ "$1" != "query" ] || [ "$2" != "default" ]; then',
' exit 1',
'fi',
'mime="$3"',
'for list in "$XDG_CONFIG_HOME/mimeapps.list" "$XDG_DATA_HOME/applications/mimeapps.list" "$XDG_DATA_HOME/applications/defaults.list"; do',
' if [ -f "$list" ]; then',
' result=$(grep "^$mime=" "$list" | head -n 1 | cut -d= -f2 | cut -d";" -f1)',
' if [ -n "$result" ]; then',
' printf "%s\\n" "$result"',
' exit 0',
' fi',
' fi',
'done',
'exit 0'
].join('\n')
);
fs.chmodSync(xdgMimePath, 0o755);
({ xdgDir, xdgDataHome, xdgConfigHome, xdgBinDir } = makeXdgMockDirectories('electron-xdg-'));
});
after(() => {
@@ -1659,6 +1619,95 @@ describe('app module', () => {
});
});
ifdescribe(process.platform === 'linux')('default protocol client APIs with mocked XDG settings', () => {
const protocol = 'electron-test-linux';
const desktopFileId = 'electron-test.desktop';
const protocolMimeType = `x-scheme-handler/${protocol}`;
let xdgDir: string;
let xdgDataHome: string;
let xdgConfigHome: string;
let xdgBinDir: string;
let oldEnv: Record<string, string | undefined>;
const getRegisteredHandler = () => {
for (const list of [
path.join(xdgConfigHome, 'mimeapps.list'),
path.join(xdgDataHome, 'applications', 'mimeapps.list'),
path.join(xdgDataHome, 'applications', 'defaults.list')
]) {
if (!fs.existsSync(list)) continue;
const match = fs
.readFileSync(list, 'utf8')
.split('\n')
.find((line) => line.startsWith(`${protocolMimeType}=`));
// foo=bar.desktop; --> bar.desktop
if (match) return match.split('=', 2)[1].split(';', 1)[0];
}
return '';
};
beforeEach(() => {
({ xdgDir, xdgDataHome, xdgConfigHome, xdgBinDir } = makeXdgMockDirectories('electron-xdg-default-client-'));
oldEnv = {
PATH: process.env.PATH,
CHROME_DESKTOP: process.env.CHROME_DESKTOP,
XDG_DATA_HOME: process.env.XDG_DATA_HOME,
XDG_DATA_DIRS: process.env.XDG_DATA_DIRS,
XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME
};
defer(() => {
for (const [key, value] of Object.entries(oldEnv)) {
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
fs.rmSync(xdgDir, { recursive: true, force: true });
});
process.env.PATH = [xdgBinDir, oldEnv.PATH].filter(Boolean).join(':');
process.env.XDG_DATA_HOME = xdgDataHome;
process.env.XDG_DATA_DIRS = [xdgDataHome, oldEnv.XDG_DATA_DIRS].filter(Boolean).join(':');
process.env.XDG_CONFIG_HOME = xdgConfigHome;
app.setDesktopName(desktopFileId);
});
it('writes the default handler to the XDG association files', async () => {
expect(getRegisteredHandler()).to.equal('');
expect(app.setAsDefaultProtocolClient(protocol)).to.equal(true);
await waitUntil(() => getRegisteredHandler() === desktopFileId);
expect(getRegisteredHandler()).to.equal(desktopFileId);
});
it('detects whether the app is the default protocol client', async () => {
expect(app.isDefaultProtocolClient(protocol)).to.equal(false);
fs.writeFileSync(
path.join(xdgConfigHome, 'mimeapps.list'),
['[Default Applications]', `${protocolMimeType}=other.desktop`].join('\n')
);
expect(app.isDefaultProtocolClient(protocol)).to.equal(false);
fs.writeFileSync(
path.join(xdgConfigHome, 'mimeapps.list'),
['[Default Applications]', `${protocolMimeType}=${desktopFileId}`].join('\n')
);
await waitUntil(() => app.isDefaultProtocolClient(protocol));
expect(app.isDefaultProtocolClient(protocol)).to.equal(true);
});
});
describe('protocol scheme validation', () => {
it('rejects empty protocol names', () => {
expect(app.setAsDefaultProtocolClient('')).to.equal(false);

View File

@@ -615,4 +615,53 @@ describe('cpp heap', () => {
);
});
});
ifdescribe(process.platform === 'darwin')('autoUpdater module', () => {
it('is retained after garbage collection', async () => {
const rc = await startRemoteControlApp(['--js-flags=--expose-gc']);
const result = await rc.remotely(async () => {
const { autoUpdater } = require('electron');
const v8Util = (process as any)._linkedBinding('electron_common_v8_util');
const wr = new WeakRef(autoUpdater);
for (let i = 0; i < 10; i++) {
await new Promise((resolve) => setTimeout(resolve, 0));
v8Util.requestGarbageCollectionForTesting();
}
return {
retained: wr.deref() !== undefined,
functional: typeof autoUpdater.getFeedURL() === 'string'
};
});
expect(result.retained).to.equal(true, 'autoUpdater should survive GC');
expect(result.functional).to.equal(true, 'autoUpdater should still be functional after GC');
});
it('should record as node in heap snapshot', async () => {
const rc = await startRemoteControlApp(['--expose-internals', '--js-flags=--expose-gc']);
const result = await rc.remotely(
async (heap: string) => {
const { autoUpdater, app } = require('electron');
const { recordState } = require(heap);
const v8Util = (process as any)._linkedBinding('electron_common_v8_util');
console.log(autoUpdater.getFeedURL());
for (let i = 0; i < 10; i++) {
await new Promise((resolve) => setTimeout(resolve, 0));
v8Util.requestGarbageCollectionForTesting();
}
const state = recordState();
const nodes = state.snapshot.filter((node: any) => node.name === 'Electron / AutoUpdater');
const found = nodes.length > 0;
const noDuplicates = nodes.length === 1;
setTimeout(() => app.quit());
return { found, noDuplicates };
},
path.join(__dirname, '../../third_party/electron_node/test/common/heap')
);
const [code] = await once(rc.process, 'exit');
expect(code).to.equal(0);
expect(result.found).to.equal(true, 'AutoUpdater should be in snapshot (held by JS module)');
expect(result.noDuplicates).to.equal(true, 'should have exactly one AutoUpdater instance');
});
});
});

View File

@@ -130,6 +130,19 @@ describe('deprecate', () => {
expect(msg).to.include('oldFn');
});
it('preserves the return value and `this` of a deprecated function with no replacement', () => {
deprecate.setHandler(() => {});
function oldFn(this: any, a: number, b: number) {
return { self: this, sum: a + b };
}
const deprecatedFn = deprecate.removeFunction(oldFn, 'oldFn');
const context = { name: 'ctx' };
const result = deprecatedFn.call(context, 2, 3);
expect(result).to.deep.equal({ self: context, sum: 5 });
});
it('warns exactly once when a function is deprecated with a replacement', () => {
let msg;
deprecate.setHandler((m) => {

15
spec/fixtures/api/xdg-mock/bin/xdg-mime vendored Normal file
View File

@@ -0,0 +1,15 @@
#!/bin/sh
if [ "$1" != "query" ] || [ "$2" != "default" ]; then
exit 1
fi
mime="$3"
for list in "$XDG_CONFIG_HOME/mimeapps.list" "$XDG_DATA_HOME/applications/mimeapps.list" "$XDG_DATA_HOME/applications/defaults.list"; do
if [ -f "$list" ]; then
result=$(grep "^$mime=" "$list" | head -n 1 | cut -d= -f2 | cut -d";" -f1)
if [ -n "$result" ]; then
printf "%s\n" "$result"
exit 0
fi
fi
done
exit 0

View File

@@ -0,0 +1,31 @@
#!/bin/sh
set -eu
get_handler() {
for list in "$XDG_CONFIG_HOME/mimeapps.list" "$XDG_DATA_HOME/applications/mimeapps.list" "$XDG_DATA_HOME/applications/defaults.list"; do
if [ -f "$list" ]; then
result=$(grep "^x-scheme-handler/$1=" "$list" | head -n 1 | cut -d= -f2 | cut -d";" -f1)
if [ -n "$result" ]; then
printf "%s\n" "$result"
return 0
fi
fi
done
return 1
}
if [ "$1" = "set" ] && [ "$2" = "default-url-scheme-handler" ]; then
mkdir -p "$XDG_CONFIG_HOME"
{
printf "[Default Applications]\n"
printf "x-scheme-handler/%s=%s\n" "$3" "$4"
} > "$XDG_CONFIG_HOME/mimeapps.list"
exit 0
fi
if [ "$1" = "check" ] && [ "$2" = "default-url-scheme-handler" ]; then
if [ "$(get_handler "$3" 2>/dev/null || true)" = "$4" ]; then
printf "yes\n"
else
printf "no\n"
fi
exit 0
fi
exit 1

View File

@@ -0,0 +1,5 @@
[Default Applications]
x-scheme-handler/mockproto=mock-browser.desktop
[Added Associations]
x-scheme-handler/mockproto=mock-browser.desktop;

View File

@@ -0,0 +1,5 @@
[Desktop Entry]
Name=Electron Test
Exec=/usr/bin/true %u
Type=Application
MimeType=x-scheme-handler/electron-test-linux;

View File

@@ -0,0 +1,5 @@
[Desktop Entry]
Name=Mock Browser
Exec=/usr/bin/true %u
Type=Application
MimeType=x-scheme-handler/mockproto;