mirror of
https://github.com/electron/electron.git
synced 2026-05-02 03:00:22 -04:00
Compare commits
7 Commits
gxu-phase3
...
fix/win-th
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26437c6202 | ||
|
|
f038413cf9 | ||
|
|
099c5c0038 | ||
|
|
2c46abe361 | ||
|
|
05e0cd085c | ||
|
|
7c56577639 | ||
|
|
350de668e2 |
15
.github/actions/build-electron/action.yml
vendored
15
.github/actions/build-electron/action.yml
vendored
@@ -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
|
||||
|
||||
132
.github/siso-patches/0002-siso-retry-transient-ERROR_INVALID_PARAMETER-when-op.patch
vendored
Normal file
132
.github/siso-patches/0002-siso-retry-transient-ERROR_INVALID_PARAMETER-when-op.patch
vendored
Normal 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
|
||||
|
||||
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -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
|
||||
|
||||
6
.github/workflows/windows-publish.yml
vendored
6
.github/workflows/windows-publish.yml
vendored
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 --"
|
||||
|
||||
@@ -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_ =
|
||||
|
||||
@@ -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())
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
15
spec/fixtures/api/xdg-mock/bin/xdg-mime
vendored
Normal 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
|
||||
31
spec/fixtures/api/xdg-mock/bin/xdg-settings
vendored
Normal file
31
spec/fixtures/api/xdg-mock/bin/xdg-settings
vendored
Normal 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
|
||||
5
spec/fixtures/api/xdg-mock/config/mimeapps.list
vendored
Normal file
5
spec/fixtures/api/xdg-mock/config/mimeapps.list
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
[Default Applications]
|
||||
x-scheme-handler/mockproto=mock-browser.desktop
|
||||
|
||||
[Added Associations]
|
||||
x-scheme-handler/mockproto=mock-browser.desktop;
|
||||
5
spec/fixtures/api/xdg-mock/data/applications/electron-test.desktop
vendored
Normal file
5
spec/fixtures/api/xdg-mock/data/applications/electron-test.desktop
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
[Desktop Entry]
|
||||
Name=Electron Test
|
||||
Exec=/usr/bin/true %u
|
||||
Type=Application
|
||||
MimeType=x-scheme-handler/electron-test-linux;
|
||||
5
spec/fixtures/api/xdg-mock/data/applications/mock-browser.desktop
vendored
Normal file
5
spec/fixtures/api/xdg-mock/data/applications/mock-browser.desktop
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
[Desktop Entry]
|
||||
Name=Mock Browser
|
||||
Exec=/usr/bin/true %u
|
||||
Type=Application
|
||||
MimeType=x-scheme-handler/mockproto;
|
||||
Reference in New Issue
Block a user