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
18 changed files with 429 additions and 117 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,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;