Compare commits

..

5 Commits

Author SHA1 Message Date
yureiblack
128880988e test: correct typo in node-spec.ts comment (#49141)
fix: correct typo in node-spec.ts comment
2026-02-25 23:03:06 -08:00
Shelley Vohr
8d05285a1f fix: potential std::stoi crash in Windows Toasts (#49947)
fix: potential std::stoi crash in Windows Toasts
2026-02-25 14:14:45 -08:00
Mitchell Cohen
6a2571ee3d ci: Wayland test job, helpers, and app spec (#49908)
* wayland test chromium patch

* ci: add wayland test job and helpers

* use weston directly instead of wlheadless-run

* roll build image to eac3529

* fixed exec command

* Update .github/workflows/pipeline-segment-electron-test.yml

Co-authored-by: John Kleinschmidt <kleinschmidtorama@gmail.com>

* Update .github/workflows/pipeline-segment-electron-test.yml

Co-authored-by: John Kleinschmidt <kleinschmidtorama@gmail.com>

* chore: fixup shard case statement

* reverted leftover patch line

---------

Co-authored-by: John Kleinschmidt <kleinschmidtorama@gmail.com>
2026-02-25 14:51:13 -05:00
Shelley Vohr
94aa90bb64 fix: recover network requests after Network Service restart (#49887)
* fix: recover network requests after Network Service restart

* chore: reuse implementation

* chore: make linter happy

* chore: fix lint

---------

Co-authored-by: deepak1556 <hop2deep@gmail.com>
Co-authored-by: Charles Kerr <charles@charleskerr.com>
2026-02-25 12:53:06 -05:00
zonescape
b9a09acff3 docs: mark "Show hidden files" file dialog setting as deprecated on Linux (#46926)
* fix: don't overwrite "Show hidden files" setting on Linux/GTK

* docs: deprecate showHiddenFiles property in dialogs on Linux

* docs: mark Electron 42 as the removal date for this feature

---------

Co-authored-by: Charles Kerr <charles@charleskerr.com>
2026-02-25 11:05:01 -05:00
33 changed files with 340 additions and 67 deletions

View File

@@ -2,7 +2,7 @@ version: '3'
services:
buildtools:
image: ghcr.io/electron/devcontainer:a82b87d7a4f5ff0cab61405f8151ac4cf4942aeb
image: ghcr.io/electron/devcontainer:eac3529546ea8f3aa356d31e345715eef342233b
volumes:
- ..:/workspaces/gclient/src/electron:cached

View File

@@ -41,7 +41,7 @@ jobs:
permissions:
contents: read
container:
image: ghcr.io/electron/build:a82b87d7a4f5ff0cab61405f8151ac4cf4942aeb
image: ghcr.io/electron/build:eac3529546ea8f3aa356d31e345715eef342233b
options: --user root
volumes:
- /mnt/cross-instance-cache:/mnt/cross-instance-cache

View File

@@ -15,7 +15,7 @@ jobs:
permissions:
contents: read
container:
image: ghcr.io/electron/build:a82b87d7a4f5ff0cab61405f8151ac4cf4942aeb
image: ghcr.io/electron/build:eac3529546ea8f3aa356d31e345715eef342233b
options: --user root
volumes:
- /mnt/cross-instance-cache:/mnt/cross-instance-cache
@@ -39,7 +39,7 @@ jobs:
permissions:
contents: read
container:
image: ghcr.io/electron/build:a82b87d7a4f5ff0cab61405f8151ac4cf4942aeb
image: ghcr.io/electron/build:eac3529546ea8f3aa356d31e345715eef342233b
options: --user root --device /dev/fuse --cap-add SYS_ADMIN
volumes:
- /mnt/win-cache:/mnt/win-cache
@@ -66,7 +66,7 @@ jobs:
# This job updates the same git cache as linux, so it needs to run after the linux one.
needs: build-git-cache-linux
container:
image: ghcr.io/electron/build:a82b87d7a4f5ff0cab61405f8151ac4cf4942aeb
image: ghcr.io/electron/build:eac3529546ea8f3aa356d31e345715eef342233b
options: --user root
volumes:
- /mnt/cross-instance-cache:/mnt/cross-instance-cache

View File

@@ -6,7 +6,7 @@ on:
build-image-sha:
type: string
description: 'SHA for electron/build image'
default: 'a82b87d7a4f5ff0cab61405f8151ac4cf4942aeb'
default: 'eac3529546ea8f3aa356d31e345715eef342233b'
required: true
skip-macos:
type: boolean
@@ -77,7 +77,7 @@ jobs:
id: set-output
run: |
if [ -z "${{ inputs.build-image-sha }}" ]; then
echo "build-image-sha=a82b87d7a4f5ff0cab61405f8151ac4cf4942aeb" >> "$GITHUB_OUTPUT"
echo "build-image-sha=eac3529546ea8f3aa356d31e345715eef342233b" >> "$GITHUB_OUTPUT"
else
echo "build-image-sha=${{ inputs.build-image-sha }}" >> "$GITHUB_OUTPUT"
fi

View File

@@ -6,7 +6,7 @@ on:
build-image-sha:
type: string
description: 'SHA for electron/build image'
default: 'a82b87d7a4f5ff0cab61405f8151ac4cf4942aeb'
default: 'eac3529546ea8f3aa356d31e345715eef342233b'
upload-to-storage:
description: 'Uploads to Azure storage'
required: false

View File

@@ -6,7 +6,7 @@ on:
build-image-sha:
type: string
description: 'SHA for electron/build image'
default: 'a82b87d7a4f5ff0cab61405f8151ac4cf4942aeb'
default: 'eac3529546ea8f3aa356d31e345715eef342233b'
required: true
upload-to-storage:
description: 'Uploads to Azure storage'

View File

@@ -110,6 +110,21 @@ jobs:
test-runs-on: ${{ inputs.test-runs-on }}
test-container: ${{ inputs.test-container }}
secrets: inherit
test-wayland:
uses: ./.github/workflows/pipeline-segment-electron-test.yml
permissions:
contents: read
issues: read
pull-requests: read
needs: build
if: ${{ inputs.target-platform == 'linux' && inputs.target-arch == 'x64' && !inputs.is-asan }}
with:
target-arch: ${{ inputs.target-arch }}
target-platform: ${{ inputs.target-platform }}
test-runs-on: ${{ inputs.test-runs-on }}
test-container: ${{ inputs.test-container }}
display-server: wayland
secrets: inherit
nn-test:
uses: ./.github/workflows/pipeline-segment-node-nan-test.yml
permissions:

View File

@@ -30,9 +30,14 @@ on:
required: false
type: boolean
default: false
display-server:
description: 'Display backend for Linux tests: x11 or wayland'
required: false
type: string
default: x11
concurrency:
group: electron-test-${{ inputs.target-platform }}-${{ inputs.target-arch }}-${{ inputs.is-asan }}-${{ github.ref_protected == true && github.run_id || github.ref }}
group: electron-test-${{ inputs.target-platform }}-${{ inputs.target-arch }}-${{ inputs.is-asan }}-${{ inputs.display-server }}-${{ github.ref_protected == true && github.run_id || github.ref }}
cancel-in-progress: ${{ github.ref_protected != true }}
permissions: {}
@@ -59,7 +64,7 @@ jobs:
fail-fast: false
matrix:
build-type: ${{ inputs.target-platform == 'macos' && fromJSON('["darwin","mas"]') || (inputs.target-platform == 'win' && fromJSON('["win"]') || fromJSON('["linux"]')) }}
shard: ${{ inputs.target-platform == 'linux' && fromJSON('[1, 2, 3]') || fromJSON('[1, 2]') }}
shard: ${{ case(inputs.display-server == 'wayland', fromJSON('[1]'), inputs.target-platform == 'linux', fromJSON('[1, 2, 3]'), fromJSON('[1, 2]')) }}
env:
BUILD_TYPE: ${{ matrix.build-type }}
TARGET_ARCH: ${{ inputs.target-arch }}
@@ -210,7 +215,22 @@ jobs:
cd src/electron
export ELECTRON_TEST_RESULTS_DIR=`pwd`/junit
# Get which tests are on this shard
tests_files=$(node script/split-tests ${{ matrix.shard }} ${{ inputs.target-platform == 'linux' && 3 || 2 }})
tests_files=$(node script/split-tests ${{ matrix.shard }} ${{ case(inputs.display-server == 'wayland', 1, inputs.target-platform == 'linux', 3, 2) }})
if [ "${{ inputs.display-server }}" = "wayland" ]; then
allowlist_file=script/wayland-test-allowlist.txt
filtered_tests=""
for test_file in $tests_files; do
if grep -Fxq "$test_file" "$allowlist_file"; then
filtered_tests="$filtered_tests $test_file"
fi
done
tests_files="${filtered_tests# }"
if [ -z "$tests_files" ]; then
echo "No tests matched Wayland filter, skipping."
exit 0
fi
fi
# Run tests
if [ "${{ inputs.target-platform }}" != "linux" ]; then
@@ -245,7 +265,11 @@ jobs:
if [ "${{ inputs.target-arch }}" = "arm" ]; then
runuser -u builduser -- xvfb-run script/actions/run-tests.sh script/yarn.js test --skipYarnInstall --runners=main --enableRerun=3 --trace-uncaught --enable-logging --files $tests_files
else
runuser -u builduser -- xvfb-run script/actions/run-tests.sh script/yarn.js test --runners=main --enableRerun=3 --trace-uncaught --enable-logging --files $tests_files
if [ "${{ inputs.display-server }}" = "wayland" ]; then
runuser -u builduser -- script/actions/run-tests-wayland.sh script/yarn.js test --runners=main --enableRerun=3 --trace-uncaught --enable-logging --files $tests_files
else
runuser -u builduser -- xvfb-run script/actions/run-tests.sh script/yarn.js test --runners=main --enableRerun=3 --trace-uncaught --enable-logging --files $tests_files
fi
fi
fi
@@ -268,7 +292,7 @@ jobs:
if: always() && !cancelled()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
with:
name: test_artifacts_${{ env.ARTIFACT_KEY }}_${{ matrix.shard }}
name: ${{ inputs.target-platform == 'linux' && format('test_artifacts_{0}_{1}_{2}', env.ARTIFACT_KEY, inputs.display-server, matrix.shard) || format('test_artifacts_{0}_{1}', env.ARTIFACT_KEY, matrix.shard) }}
path: src/electron/spec/artifacts
if-no-files-found: ignore
- name: Wait for active SSH sessions

View File

@@ -6,7 +6,7 @@ on:
build-image-sha:
type: string
description: 'SHA for electron/build image'
default: 'a82b87d7a4f5ff0cab61405f8151ac4cf4942aeb'
default: 'eac3529546ea8f3aa356d31e345715eef342233b'
required: true
upload-to-storage:
description: 'Uploads to Azure storage'

View File

@@ -30,7 +30,7 @@ The `dialog` module has the following methods:
* `openFile` - Allow files to be selected.
* `openDirectory` - Allow directories to be selected.
* `multiSelections` - Allow multiple paths to be selected.
* `showHiddenFiles` - Show hidden files in dialog.
* `showHiddenFiles` _macOS_ _Windows_ _Deprecated_ - Show hidden files in dialog. Deprecated on Linux.
* `createDirectory` _macOS_ - Allow creating new directories from dialog.
* `promptToCreate` _Windows_ - Prompt for creation if the file path entered
in the dialog does not exist. This does not actually create the file at
@@ -102,7 +102,7 @@ dialog.showOpenDialogSync(mainWindow, {
* `openFile` - Allow files to be selected.
* `openDirectory` - Allow directories to be selected.
* `multiSelections` - Allow multiple paths to be selected.
* `showHiddenFiles` - Show hidden files in dialog.
* `showHiddenFiles` _macOS_ _Windows_ _Deprecated_ - Show hidden files in dialog. Deprecated on Linux.
* `createDirectory` _macOS_ - Allow creating new directories from dialog.
* `promptToCreate` _Windows_ - Prompt for creation if the file path entered
in the dialog does not exist. This does not actually create the file at
@@ -185,7 +185,7 @@ dialog.showOpenDialog(mainWindow, {
* `showsTagField` boolean (optional) _macOS_ - Show the tags input box,
defaults to `true`.
* `properties` string[]&#32;(optional)
* `showHiddenFiles` - Show hidden files in dialog.
* `showHiddenFiles` _macOS_ _Windows_ _Deprecated_ - Show hidden files in dialog. Deprecated on Linux.
* `createDirectory` _macOS_ - Allow creating new directories from dialog.
* `treatPackageAsDirectory` _macOS_ - Treat packages, such as `.app` folders,
as a directory instead of a file.
@@ -215,7 +215,7 @@ The `filters` specifies an array of file types that can be displayed, see
displayed in front of the filename text field.
* `showsTagField` boolean (optional) _macOS_ - Show the tags input box, defaults to `true`.
* `properties` string[]&#32;(optional)
* `showHiddenFiles` - Show hidden files in dialog.
* `showHiddenFiles` _macOS_ _Windows_ _Deprecated_ - Show hidden files in dialog. Deprecated on Linux.
* `createDirectory` _macOS_ - Allow creating new directories from dialog.
* `treatPackageAsDirectory` _macOS_ - Treat packages, such as `.app` folders,
as a directory instead of a file.

View File

@@ -80,6 +80,12 @@ your preload script and expose it using the [contextBridge](https://www.electron
Debug symbols for MacOS (dSYM) now use xz compression in order to handle larger file sizes. `dsym.zip` files are now
`dsym.tar.xz` files. End users using debug symbols may need to update their zip utilities.
### Deprecated: `showHiddenFiles` in Dialogs on Linux
This property will still be honored on macOS and Windows, but support on Linux
will be removed in Electron 42. GTK intends for this to be a user choice rather
than an app choice and has removed the API to do this programmatically.
## Planned Breaking API Changes (39.0)
### Deprecated: `--host-rules` command line switch

View File

@@ -144,3 +144,4 @@ fix_linux_tray_id.patch
expose_gtk_ui_platform_field.patch
patch_osr_control_screen_info.patch
refactor_allow_customizing_config_in_freedesktopsecretkeyprovider.patch
fix_wayland_test_crash_on_teardown.patch

View File

@@ -0,0 +1,20 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Mitchell Cohen <mitch.cohen@me.com>
Date: Sun, 22 Feb 2026 11:38:49 -0500
Subject: fix: Wayland test crash on teardown
Allows Wayland test job to teardown the connection without crashing when trying to update the pointer
diff --git a/ui/ozone/platform/wayland/host/wayland_connection.cc b/ui/ozone/platform/wayland/host/wayland_connection.cc
index 4c44eaeebe091906a1676da106faa9072819b67e..224f6abfe06794d31fc4d876c8242dab79ba075d 100644
--- a/ui/ozone/platform/wayland/host/wayland_connection.cc
+++ b/ui/ozone/platform/wayland/host/wayland_connection.cc
@@ -426,7 +426,7 @@ std::vector<TouchscreenDevice> WaylandConnection::CreateTouchscreenDevices()
}
void WaylandConnection::UpdateCursor() {
- if (auto* pointer = seat_->pointer()) {
+ if (auto* pointer = seat_ ? seat_->pointer() : nullptr) {
cursor_ = std::make_unique<WaylandCursor>(pointer, this);
cursor_->set_listener(listener_);
cursor_position_ = std::make_unique<WaylandCursorPosition>();

View File

@@ -0,0 +1,30 @@
#!/bin/bash
set -euo pipefail
export XDG_SESSION_TYPE=wayland
export WAYLAND_DISPLAY="${WAYLAND_DISPLAY:-wayland-99}"
if [[ -z "${XDG_RUNTIME_DIR:-}" ]]; then
XDG_RUNTIME_DIR="$(mktemp -d)"
chmod 700 "$XDG_RUNTIME_DIR"
export XDG_RUNTIME_DIR
trap 'kill "$WESTON_PID" >/dev/null 2>&1 || true; rm -rf "$XDG_RUNTIME_DIR"' EXIT
else
trap 'kill "$WESTON_PID" >/dev/null 2>&1 || true' EXIT
fi
weston \
--backend=headless-backend.so \
--socket="$WAYLAND_DISPLAY" \
--idle-time=0 \
>/tmp/weston-headless.log 2>&1 &
WESTON_PID=$!
for _ in {1..100}; do
if [[ -S "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" ]]; then
break
fi
sleep 0.1
done
node "$@" --ozone-platform=wayland

View File

@@ -0,0 +1,4 @@
spec/parse-features-string-spec.ts
spec/types-spec.ts
spec/version-bump-spec.ts
spec/api-app-spec.ts

View File

@@ -9,7 +9,6 @@
#include "base/environment.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "content/public/browser/browser_task_traits.h"
@@ -280,8 +279,7 @@ void OnDeploymentCompleted(std::unique_ptr<DeploymentCallbackData> data,
HRESULT error_code;
hr = async_info->get_ErrorCode(&error_code);
if (SUCCEEDED(hr)) {
error +=
" (" + base::NumberToString(static_cast<int>(error_code)) + ")";
error += " (" + std::to_string(static_cast<int>(error_code)) + ")";
}
}
}
@@ -800,10 +798,10 @@ v8::Local<v8::Value> GetPackageInfo() {
ABI::Windows::ApplicationModel::PackageVersion pkg_version;
hr = package_id->get_Version(&pkg_version);
if (SUCCEEDED(hr)) {
std::string version = base::NumberToString(pkg_version.Major) + "." +
base::NumberToString(pkg_version.Minor) + "." +
base::NumberToString(pkg_version.Build) + "." +
base::NumberToString(pkg_version.Revision);
std::string version = std::to_string(pkg_version.Major) + "." +
std::to_string(pkg_version.Minor) + "." +
std::to_string(pkg_version.Build) + "." +
std::to_string(pkg_version.Revision);
result.Set("version", version);
}
}

View File

@@ -14,6 +14,7 @@
#include "base/process/launch.h"
#include "base/process/process.h"
#include "chrome/browser/browser_process.h"
#include "content/browser/network_service_instance_impl.h" // nogncheck
#include "content/public/browser/child_process_host.h"
#include "content/public/browser/service_process_host.h"
#include "content/public/common/result_codes.h"
@@ -72,7 +73,8 @@ UtilityProcessWrapper::UtilityProcessWrapper(
base::FilePath current_working_directory,
bool use_plugin_helper,
bool create_network_observer,
bool disclaim_responsibility) {
bool disclaim_responsibility)
: create_network_observer_(create_network_observer) {
#if BUILDFLAG(IS_WIN)
base::win::ScopedHandle stdout_write(nullptr);
base::win::ScopedHandle stderr_write(nullptr);
@@ -212,32 +214,15 @@ UtilityProcessWrapper::UtilityProcessWrapper(
connector_->set_connection_error_handler(base::BindOnce(
&UtilityProcessWrapper::CloseConnectorPort, weak_factory_.GetWeakPtr()));
mojo::PendingRemote<network::mojom::URLLoaderFactory> url_loader_factory;
network::mojom::URLLoaderFactoryParamsPtr loader_params =
network::mojom::URLLoaderFactoryParams::New();
loader_params->process_id = network::OriginatingProcess::browser();
loader_params->is_orb_enabled = false;
loader_params->is_trusted = true;
if (create_network_observer) {
url_loader_network_observer_.emplace();
loader_params->url_loader_network_observer =
url_loader_network_observer_->Bind();
}
network::mojom::NetworkContext* network_context =
g_browser_process->system_network_context_manager()->GetContext();
network_context->CreateURLLoaderFactory(
url_loader_factory.InitWithNewPipeAndPassReceiver(),
std::move(loader_params));
params->url_loader_factory = std::move(url_loader_factory);
mojo::PendingRemote<network::mojom::HostResolver> host_resolver;
network_context->CreateHostResolver(
{}, host_resolver.InitWithNewPipeAndPassReceiver());
params->host_resolver = std::move(host_resolver);
params->use_network_observer_from_url_loader_factory =
create_network_observer;
params->url_loader_factory_params = CreateURLLoaderFactoryParams();
node_service_remote_->Initialize(std::move(params),
receiver_.BindNewPipeAndPassRemote());
// Subscribe to Network Service process gone notifications.
network_service_gone_subscription_ =
content::RegisterNetworkServiceProcessGoneHandler(base::BindRepeating(
&UtilityProcessWrapper::CreateAndSendURLLoaderFactory,
weak_factory_.GetWeakPtr()));
}
UtilityProcessWrapper::~UtilityProcessWrapper() {
@@ -429,6 +414,44 @@ void UtilityProcessWrapper::OnV8FatalError(const std::string& location,
EmitWithoutEvent("error", "FatalError", location, report);
}
void UtilityProcessWrapper::CreateAndSendURLLoaderFactory(bool /* crashed */) {
if (!node_service_remote_.is_connected())
return;
node_service_remote_->UpdateURLLoaderFactory(CreateURLLoaderFactoryParams());
}
node::mojom::URLLoaderFactoryParamsPtr
UtilityProcessWrapper::CreateURLLoaderFactoryParams() {
node::mojom::URLLoaderFactoryParamsPtr params =
node::mojom::URLLoaderFactoryParams::New();
mojo::PendingRemote<network::mojom::URLLoaderFactory> url_loader_factory;
network::mojom::URLLoaderFactoryParamsPtr loader_params =
network::mojom::URLLoaderFactoryParams::New();
loader_params->process_id = network::OriginatingProcess::browser();
loader_params->is_orb_enabled = false;
loader_params->is_trusted = true;
if (create_network_observer_) {
url_loader_network_observer_.emplace();
loader_params->url_loader_network_observer =
url_loader_network_observer_->Bind();
}
network::mojom::NetworkContext* network_context =
g_browser_process->system_network_context_manager()->GetContext();
network_context->CreateURLLoaderFactory(
url_loader_factory.InitWithNewPipeAndPassReceiver(),
std::move(loader_params));
params->url_loader_factory = std::move(url_loader_factory);
mojo::PendingRemote<network::mojom::HostResolver> host_resolver;
network_context->CreateHostResolver(
{}, host_resolver.InitWithNewPipeAndPassReceiver());
params->host_resolver = std::move(host_resolver);
params->use_network_observer_from_url_loader_factory =
create_network_observer_;
return params;
}
// static
raw_ptr<UtilityProcessWrapper> UtilityProcessWrapper::FromProcessId(
base::ProcessId pid) {

View File

@@ -9,6 +9,7 @@
#include <memory>
#include <string>
#include "base/callback_list.h"
#include "base/containers/id_map.h"
#include "base/environment.h"
#include "base/memory/weak_ptr.h"
@@ -99,6 +100,11 @@ class UtilityProcessWrapper final
void OnServiceProcessDisconnected(uint32_t exit_code,
const std::string& description);
// Creates and sends a new URLLoaderFactory to the utility process.
// Called after Network Service restart to update the factory.
void CreateAndSendURLLoaderFactory(bool crashed);
node::mojom::URLLoaderFactoryParamsPtr CreateURLLoaderFactoryParams();
base::ProcessId pid_ = base::kNullProcessId;
#if BUILDFLAG(IS_WIN)
// Non-owning handles, these will be closed when the
@@ -111,12 +117,14 @@ class UtilityProcessWrapper final
bool connector_closed_ = false;
bool terminated_ = false;
bool killed_ = false;
bool create_network_observer_ = false;
std::unique_ptr<mojo::Connector> connector_;
blink::MessagePortDescriptor host_port_;
mojo::Receiver<node::mojom::NodeServiceClient> receiver_{this};
mojo::Remote<node::mojom::NodeService> node_service_remote_;
std::optional<electron::URLLoaderNetworkObserver>
url_loader_network_observer_;
base::CallbackListSubscription network_service_gone_subscription_;
base::WeakPtrFactory<UtilityProcessWrapper> weak_factory_{this};
};

View File

@@ -23,7 +23,6 @@
#include "base/json/json_reader.h"
#include "base/no_destructor.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/current_thread.h"
#include "base/threading/scoped_blocking_call.h"
@@ -2660,7 +2659,7 @@ void WebContents::RestoreHistory(
thrower.ThrowError(
"Failed to restore navigation history: Invalid navigation entry at "
"index " +
base::NumberToString(index) + ".");
std::to_string(index) + ".");
return;
}

View File

@@ -30,6 +30,7 @@
#include "components/proxy_config/pref_proxy_config_tracker_impl.h"
#include "components/proxy_config/proxy_config_pref_names.h"
#include "content/browser/blob_storage/chrome_blob_storage_context.h" // nogncheck
#include "content/browser/network_service_instance_impl.h" // nogncheck
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/cors_origin_pattern_setter.h"
#include "content/public/browser/host_zoom_map.h"
@@ -407,10 +408,18 @@ ElectronBrowserContext::ElectronBrowserContext(
extension_system->FinishInitialization();
}
#endif
// Subscribe to Network Service process gone notifications to reset the
// cached URLLoaderFactory when the Network Service crashes or restarts.
network_service_gone_subscription_ =
content::RegisterNetworkServiceProcessGoneHandler(base::BindRepeating(
&ElectronBrowserContext::OnNetworkServiceProcessGone,
weak_factory_.GetWeakPtr()));
}
ElectronBrowserContext::~ElectronBrowserContext() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
NotifyWillBeDestroyed();
// Notify any keyed services of browser context destruction.
@@ -568,6 +577,12 @@ content::PreconnectManager* ElectronBrowserContext::GetPreconnectManager() {
return preconnect_manager_.get();
}
void ElectronBrowserContext::OnNetworkServiceProcessGone(bool /* crashed */) {
// Clear the cached URLLoaderFactory so the next request creates a new one
// from the new NetworkContext.
url_loader_factory_.reset();
}
scoped_refptr<network::SharedURLLoaderFactory>
ElectronBrowserContext::GetURLLoaderFactory() {
if (url_loader_factory_)

View File

@@ -13,7 +13,9 @@
#include <variant>
#include <vector>
#include "base/callback_list.h"
#include "base/files/file_path.h"
#include "base/memory/weak_ptr.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/media_stream_request.h"
#include "mojo/public/cpp/bindings/remote.h"
@@ -184,6 +186,9 @@ class ElectronBrowserContext : public content::BrowserContext {
// Initialize pref registry.
void InitPrefs();
// Called when the Network Service process crashes or restarts.
void OnNetworkServiceProcessGone(bool crashed);
scoped_refptr<ValueMapPrefStore> in_memory_pref_store_;
std::unique_ptr<CookieChangeNotifier> cookie_change_notifier_;
std::unique_ptr<PrefService> prefs_;
@@ -207,6 +212,9 @@ class ElectronBrowserContext : public content::BrowserContext {
// Shared URLLoaderFactory.
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
// Subscription to Network Service process gone notifications.
base::CallbackListSubscription network_service_gone_subscription_;
network::mojom::SSLConfigPtr ssl_config_;
mojo::Remote<network::mojom::SSLConfigClient> ssl_config_client_;
@@ -214,6 +222,8 @@ class ElectronBrowserContext : public content::BrowserContext {
// In-memory cache that holds objects that have been granted permissions.
DevicePermissionMap granted_devices_;
base::WeakPtrFactory<ElectronBrowserContext> weak_factory_{this};
};
} // namespace electron

View File

@@ -371,7 +371,7 @@ void HandleToastActivation(const std::wstring& invoked_args,
int action_index = -1;
if (!action_index_str.empty()) {
action_index = std::stoi(action_index_str);
base::StringToInt(base::WideToUTF8(action_index_str), &action_index);
}
std::string reply_text;

View File

@@ -5,8 +5,12 @@
#include "base/command_line.h"
#include "base/dcheck_is_on.h"
#include "base/logging.h"
#include "content/browser/network_service_instance_impl.h" // nogncheck
#include "content/public/browser/network_service_instance.h"
#include "content/public/common/content_switches.h"
#include "shell/common/callback_util.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/promise.h"
#include "shell/common/node_includes.h"
#include "v8/include/v8.h"
@@ -41,6 +45,18 @@ std::string GetLoggingDestination() {
return command_line->GetSwitchValueASCII(switches::kEnableLogging);
}
v8::Local<v8::Promise> SimulateNetworkServiceCrash(v8::Isolate* isolate) {
gin_helper::Promise<void> promise(isolate);
v8::Local<v8::Promise> handle = promise.GetHandle();
auto subscription = content::RegisterNetworkServiceProcessGoneHandler(
electron::AdaptCallbackForRepeating(
base::BindOnce([](gin_helper::Promise<void> promise,
bool crashed) { promise.Resolve(); },
std::move(promise))));
content::RestartNetworkService();
return handle;
}
void Initialize(v8::Local<v8::Object> exports,
v8::Local<v8::Value> unused,
v8::Local<v8::Context> context,
@@ -49,6 +65,7 @@ void Initialize(v8::Local<v8::Object> exports,
gin_helper::Dictionary dict{isolate, exports};
dict.SetMethod("log", &Log);
dict.SetMethod("getLoggingDestination", &GetLoggingDestination);
dict.SetMethod("simulateNetworkServiceCrash", &SimulateNetworkServiceCrash);
}
} // namespace

View File

@@ -484,6 +484,7 @@ void SimpleURLLoaderWrapper::Clone(
void SimpleURLLoaderWrapper::Cancel() {
loader_.reset();
url_loader_factory_.reset();
pinned_wrapper_.Reset();
pinned_chunk_pipe_getter_.Reset();
// This ensures that no further callbacks will be called, so there's no need
@@ -750,6 +751,7 @@ void SimpleURLLoaderWrapper::OnComplete(bool success) {
// we would perform cleanup of the wrapper and we should bail out below.
if (self) {
loader_.reset();
url_loader_factory_.reset();
pinned_wrapper_.Reset();
pinned_chunk_pipe_getter_.Reset();
}

View File

@@ -18,7 +18,6 @@
#include "base/environment.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
@@ -193,7 +192,7 @@ void V8OOMErrorCallback(const char* location, const v8::OOMDetails& details) {
#if !IS_MAS_BUILD()
electron::crash_keys::SetCrashKey("electron.v8-oom.is_heap_oom",
base::NumberToString(details.is_heap_oom));
std::to_string(details.is_heap_oom));
if (location) {
electron::crash_keys::SetCrashKey("electron.v8-oom.location", location);
}

View File

@@ -122,10 +122,9 @@ void NodeService::Initialize(
ParentPort::GetInstance()->Initialize(std::move(params->port));
URLLoaderBundle::GetInstance()->SetURLLoaderFactory(
std::move(params->url_loader_factory),
mojo::Remote(std::move(params->host_resolver)),
params->use_network_observer_from_url_loader_factory);
if (params->url_loader_factory_params) {
UpdateURLLoaderFactory(std::move(params->url_loader_factory_params));
}
js_env_ = std::make_unique<JavascriptEnvironment>(node_bindings_->uv_loop());
@@ -208,4 +207,12 @@ void NodeService::Initialize(
node_bindings_->StartPolling();
}
void NodeService::UpdateURLLoaderFactory(
node::mojom::URLLoaderFactoryParamsPtr params) {
URLLoaderBundle::GetInstance()->SetURLLoaderFactory(
std::move(params->url_loader_factory),
mojo::Remote(std::move(params->host_resolver)),
params->use_network_observer_from_url_loader_factory);
}
} // namespace electron

View File

@@ -65,6 +65,8 @@ class NodeService : public node::mojom::NodeService {
void Initialize(node::mojom::NodeServiceParamsPtr params,
mojo::PendingRemote<node::mojom::NodeServiceClient>
client_pending_remote) override;
void UpdateURLLoaderFactory(
node::mojom::URLLoaderFactoryParamsPtr params) override;
private:
// This needs to be initialized first so that it can be destroyed last

View File

@@ -10,14 +10,18 @@ import "services/network/public/mojom/host_resolver.mojom";
import "services/network/public/mojom/url_loader_factory.mojom";
import "third_party/blink/public/mojom/messaging/message_port_descriptor.mojom";
struct URLLoaderFactoryParams {
pending_remote<network.mojom.URLLoaderFactory> url_loader_factory;
pending_remote<network.mojom.HostResolver> host_resolver;
bool use_network_observer_from_url_loader_factory = false;
};
struct NodeServiceParams {
mojo_base.mojom.FilePath script;
array<string> args;
array<string> exec_args;
blink.mojom.MessagePortDescriptor port;
pending_remote<network.mojom.URLLoaderFactory> url_loader_factory;
pending_remote<network.mojom.HostResolver> host_resolver;
bool use_network_observer_from_url_loader_factory = false;
URLLoaderFactoryParams url_loader_factory_params;
};
interface NodeServiceClient {
@@ -28,4 +32,6 @@ interface NodeServiceClient {
interface NodeService {
Initialize(NodeServiceParams params,
pending_remote<NodeServiceClient> client_remote);
UpdateURLLoaderFactory(URLLoaderFactoryParams params);
};

View File

@@ -15,7 +15,7 @@ import { setTimeout } from 'node:timers/promises';
import { promisify } from 'node:util';
import { collectStreamBody, getResponse } from './lib/net-helpers';
import { ifdescribe, ifit, listen, waitUntil } from './lib/spec-helpers';
import { ifdescribe, ifit, isWayland, listen, waitUntil } from './lib/spec-helpers';
import { closeWindow, closeAllWindows } from './lib/window-helpers';
const fixturesPath = path.resolve(__dirname, 'fixtures');
@@ -587,7 +587,7 @@ describe('app module', () => {
});
// FIXME: re-enable this test on win32.
ifit(process.platform !== 'win32')('should emit render-process-gone event when renderer crashes', async () => {
ifit(process.platform !== 'win32' && !isWayland)('should emit render-process-gone event when renderer crashes', async () => {
w = new BrowserWindow({
show: false,
webPreferences: {
@@ -1437,7 +1437,7 @@ describe('app module', () => {
describe('getApplicationNameForProtocol()', () => {
// TODO: Linux CI doesn't have registered http & https handlers
ifit(!(process.env.CI && process.platform === 'linux'))('returns application names for common protocols', function () {
ifit(!(process.env.CI && process.platform === 'linux') && !isWayland)('returns application names for common protocols', function () {
// We can't expect particular app names here, but these protocols should
// at least have _something_ registered. Except on our Linux CI
// environment apparently.

View File

@@ -1688,4 +1688,61 @@ describe('net module', () => {
}
});
}
describe('Network Service crash recovery', () => {
const binding = process._linkedBinding('electron_common_testing');
it('should recover net.fetch after Network Service crash (main process)', async () => {
const serverUrl = await respondOnce.toSingleURL((request, response) => {
response.end('first');
});
const firstResponse = await net.fetch(serverUrl);
expect(firstResponse.ok).to.be.true();
expect(await firstResponse.text()).to.equal('first');
await binding.simulateNetworkServiceCrash();
// Wait for StoragePartitionImpl's NetworkContext disconnect handler to
// fire and reinitialize the context in the new Network Service.
await setTimeout(500);
const secondServerUrl = await respondOnce.toSingleURL((request, response) => {
response.end('second');
});
const secondResponse = await net.fetch(secondServerUrl);
expect(secondResponse.ok).to.be.true();
expect(await secondResponse.text()).to.equal('second');
});
it('should recover net.fetch after Network Service crash (utility process)', async () => {
const child = utilityProcess.fork(path.join(fixturesPath, 'api', 'utility-process', 'network-restart-test.js'));
await once(child, 'spawn');
await once(child, 'message');
const firstServerUrl = await respondOnce.toSingleURL((request, response) => {
response.end('utility-first');
});
child.postMessage({ type: 'fetch', url: firstServerUrl });
const [firstResult] = await once(child, 'message');
expect(firstResult.ok).to.be.true();
expect(firstResult.body).to.equal('utility-first');
await binding.simulateNetworkServiceCrash();
// Needed for UpdateURLLoaderFactory IPC to propagate to the utility process
// and for any in-flight requests to settle
await setTimeout(500);
const secondServerUrl = await respondOnce.toSingleURL((request, response) => {
response.end('utility-second');
});
child.postMessage({ type: 'fetch', url: secondServerUrl });
const [secondResult] = await once(child, 'message');
expect(secondResult.ok).to.be.true();
expect(secondResult.body).to.equal('utility-second');
child.kill();
await once(child, 'exit');
});
});
});

View File

@@ -0,0 +1,24 @@
const { net } = require('electron');
process.parentPort.on('message', async (e) => {
const { type, url } = e.data;
if (type === 'fetch') {
try {
const response = await net.fetch(url);
const body = await response.text();
process.parentPort.postMessage({
ok: response.ok,
status: response.status,
body
});
} catch (error) {
process.parentPort.postMessage({
ok: false,
error: error.message
});
}
}
});
process.parentPort.postMessage({ type: 'ready' });

View File

@@ -25,6 +25,12 @@ const addOnly = <T>(fn: Function): T => {
export const ifit = (condition: boolean) => (condition ? it : addOnly<TestFunction>(it.skip));
export const ifdescribe = (condition: boolean) => (condition ? describe : addOnly<SuiteFunction>(describe.skip));
export const isWayland = process.platform === 'linux' && (
process.env.XDG_SESSION_TYPE === 'wayland' ||
!!process.env.WAYLAND_DISPLAY ||
process.argv.includes('--ozone-platform=wayland')
);
type CleanupFunction = (() => void) | (() => Promise<void>)
const cleanupFunctions: CleanupFunction[] = [];
export async function runCleanupFunctions () {

View File

@@ -28,7 +28,7 @@ describe('node feature', () => {
expect(msg).to.equal('message');
});
it('Has its module searth paths restricted', async () => {
it('Has its module search paths restricted', async () => {
const child = childProcess.fork(path.join(fixtures, 'module', 'module-paths.js'));
const [msg] = await once(child, 'message');
expect(msg.length).to.equal(2);