refactor: api::utilityProcessWrapper managed by cppgc (#50955)

* refactor: api::utilityProcessWrapper managed by cppgc

* chore: fix lint
This commit is contained in:
Robo 2026-04-13 17:46:51 +09:00 committed by GitHub
commit b9e462f397
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 232 additions and 61 deletions

View file

@ -5,6 +5,7 @@
#include "shell/browser/api/electron_api_utility_process.h"
#include <map>
#include <unordered_map>
#include <utility>
#include "base/files/file_util.h"
@ -19,6 +20,7 @@
#include "content/public/browser/service_process_host.h"
#include "content/public/common/result_codes.h"
#include "gin/object_template_builder.h"
#include "gin/persistent.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "services/network/public/cpp/originating_process_id.h"
#include "shell/browser/api/message_port.h"
@ -31,11 +33,13 @@
#include "shell/common/gin_helper/dictionary.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 "shell/common/v8_util.h"
#include "third_party/blink/public/common/messaging/message_port_descriptor.h"
#include "third_party/blink/public/common/messaging/transferable_message_mojom_traits.h"
#include "third_party/blink/public/mojom/blob/blob.mojom.h"
#include "v8/include/cppgc/allocation.h"
#if BUILDFLAG(IS_POSIX)
#include "base/posix/eintr_wrapper.h"
@ -51,20 +55,34 @@ namespace electron {
namespace {
base::IDMap<api::UtilityProcessWrapper*, base::ProcessId>&
GetAllUtilityProcessWrappers() {
static base::NoDestructor<
base::IDMap<api::UtilityProcessWrapper*, base::ProcessId>>
s_all_utility_process_wrappers;
return *s_all_utility_process_wrappers;
// Maps process IDs to their UtilityProcessWrapper instances.
struct UtilityProcessRegistry {
void Add(base::ProcessId pid, api::UtilityProcessWrapper* wrapper) {
map_.emplace(pid, wrapper);
}
void Remove(base::ProcessId pid) { map_.erase(pid); }
api::UtilityProcessWrapper* Lookup(base::ProcessId pid) {
auto it = map_.find(pid);
return it != map_.end() ? it->second.Get() : nullptr;
}
private:
std::unordered_map<base::ProcessId,
cppgc::WeakPersistent<api::UtilityProcessWrapper>>
map_;
};
UtilityProcessRegistry& GetAllUtilityProcessWrappers() {
static base::NoDestructor<UtilityProcessRegistry> registry;
return *registry;
}
} // namespace
namespace api {
gin::DeprecatedWrapperInfo UtilityProcessWrapper::kWrapperInfo = {
gin::kEmbedderNativeGin};
const gin::WrapperInfo UtilityProcessWrapper::kWrapperInfo =
electron::MakeWrapperInfo(electron::kElectronUtilityProcess);
UtilityProcessWrapper::UtilityProcessWrapper(
node::mojom::NodeServiceParamsPtr params,
@ -76,6 +94,8 @@ UtilityProcessWrapper::UtilityProcessWrapper(
bool create_network_observer,
bool disclaim_responsibility)
: create_network_observer_(create_network_observer) {
auto& allocation_handle =
JavascriptEnvironment::GetIsolate()->GetCppHeap()->GetAllocationHandle();
#if BUILDFLAG(IS_WIN)
base::win::ScopedHandle stdout_write(nullptr);
base::win::ScopedHandle stderr_write(nullptr);
@ -194,12 +214,13 @@ UtilityProcessWrapper::UtilityProcessWrapper(
#endif
.WithProcessCallback(
base::BindOnce(&UtilityProcessWrapper::OnServiceProcessLaunch,
weak_factory_.GetWeakPtr()))
gin::WrapPersistent(
weak_factory_.GetWeakCell(allocation_handle))))
.Pass());
node_service_remote_.set_disconnect_with_reason_handler(
base::BindOnce(&UtilityProcessWrapper::OnServiceProcessDisconnected,
weak_factory_.GetWeakPtr()));
node_service_remote_.set_disconnect_with_reason_handler(base::BindOnce(
&UtilityProcessWrapper::OnServiceProcessDisconnected,
gin::WrapPersistent(weak_factory_.GetWeakCell(allocation_handle))));
// We use a separate message pipe to support postMessage API
// instead of the existing receiver interface so that we can
@ -214,7 +235,8 @@ UtilityProcessWrapper::UtilityProcessWrapper(
base::SingleThreadTaskRunner::GetCurrentDefault());
connector_->set_incoming_receiver(this);
connector_->set_connection_error_handler(base::BindOnce(
&UtilityProcessWrapper::CloseConnectorPort, weak_factory_.GetWeakPtr()));
&UtilityProcessWrapper::CloseConnectorPort,
gin::WrapPersistent(weak_factory_.GetWeakCell(allocation_handle))));
params->url_loader_factory_params = CreateURLLoaderFactoryParams();
node_service_remote_->Initialize(std::move(params),
@ -224,7 +246,7 @@ UtilityProcessWrapper::UtilityProcessWrapper(
network_service_gone_subscription_ =
content::RegisterNetworkServiceProcessGoneHandler(base::BindRepeating(
&UtilityProcessWrapper::CreateAndSendURLLoaderFactory,
weak_factory_.GetWeakPtr()));
gin::WrapPersistent(weak_factory_.GetWeakCell(allocation_handle))));
}
UtilityProcessWrapper::~UtilityProcessWrapper() {
@ -235,7 +257,7 @@ void UtilityProcessWrapper::OnServiceProcessLaunch(
const base::Process& process) {
DCHECK(node_service_remote_.is_connected());
pid_ = process.Pid();
GetAllUtilityProcessWrappers().AddWithID(this, pid_);
GetAllUtilityProcessWrappers().Add(pid_, this);
if (stdout_read_fd_ != -1)
EmitWithoutEvent("stdout", stdout_read_fd_);
if (stderr_read_fd_ != -1)
@ -276,7 +298,7 @@ void UtilityProcessWrapper::HandleTermination(uint32_t exit_code) {
#endif
}
EmitWithoutEvent("exit", exit_code);
Unpin();
keep_alive_.Clear();
}
void UtilityProcessWrapper::OnServiceProcessDisconnected(
@ -455,25 +477,25 @@ UtilityProcessWrapper::CreateURLLoaderFactoryParams() {
}
// static
raw_ptr<UtilityProcessWrapper> UtilityProcessWrapper::FromProcessId(
UtilityProcessWrapper* UtilityProcessWrapper::FromProcessId(
base::ProcessId pid) {
auto* utility_process_wrapper = GetAllUtilityProcessWrappers().Lookup(pid);
return !!utility_process_wrapper ? utility_process_wrapper : nullptr;
return utility_process_wrapper ? utility_process_wrapper : nullptr;
}
// static
gin_helper::Handle<UtilityProcessWrapper> UtilityProcessWrapper::Create(
UtilityProcessWrapper* UtilityProcessWrapper::Create(
gin::Arguments* const args) {
if (!Browser::Get()->is_ready()) {
args->ThrowTypeError(
"utilityProcess cannot be created before app is ready.");
return {};
return nullptr;
}
gin_helper::Dictionary dict;
if (!args->GetNext(&dict)) {
args->ThrowTypeError("Options must be an object.");
return {};
return nullptr;
}
std::u16string display_name;
@ -488,19 +510,19 @@ gin_helper::Handle<UtilityProcessWrapper> UtilityProcessWrapper::Create(
dict.Get("modulePath", &params->script);
if (dict.Has("args") && !dict.Get("args", &params->args)) {
args->ThrowTypeError("Invalid value for args");
return {};
return nullptr;
}
gin_helper::Dictionary opts;
if (dict.Get("options", &opts)) {
if (opts.Has("env") && !opts.Get("env", &env_map)) {
args->ThrowTypeError("Invalid value for env");
return {};
return nullptr;
}
if (opts.Has("execArgv") && !opts.Get("execArgv", &params->exec_args)) {
args->ThrowTypeError("Invalid value for execArgv");
return {};
return nullptr;
}
opts.Get("serviceName", &display_name);
@ -526,17 +548,13 @@ gin_helper::Handle<UtilityProcessWrapper> UtilityProcessWrapper::Create(
opts.Get("disclaim", &disclaim_responsibility);
#endif
}
auto handle = gin_helper::CreateHandle(
args->isolate(),
new UtilityProcessWrapper(
std::move(params), display_name, std::move(stdio), env_map,
current_working_directory, use_plugin_helper, create_network_observer,
disclaim_responsibility));
handle->Pin(args->isolate());
return handle;
v8::Isolate* isolate = args->isolate();
return cppgc::MakeGarbageCollected<UtilityProcessWrapper>(
isolate->GetCppHeap()->GetAllocationHandle(), std::move(params),
display_name, std::move(stdio), env_map, current_working_directory,
use_plugin_helper, create_network_observer, disclaim_responsibility);
}
// static
gin::ObjectTemplateBuilder UtilityProcessWrapper::GetObjectTemplateBuilder(
v8::Isolate* isolate) {
return gin_helper::EventEmitterMixin<
@ -546,8 +564,17 @@ gin::ObjectTemplateBuilder UtilityProcessWrapper::GetObjectTemplateBuilder(
.SetProperty("pid", &UtilityProcessWrapper::GetOSProcessId);
}
const char* UtilityProcessWrapper::GetTypeName() {
return "UtilityProcessWrapper";
void UtilityProcessWrapper::Trace(cppgc::Visitor* visitor) const {
gin::Wrappable<UtilityProcessWrapper>::Trace(visitor);
visitor->Trace(weak_factory_);
}
const gin::WrapperInfo* UtilityProcessWrapper::wrapper_info() const {
return &kWrapperInfo;
}
const char* UtilityProcessWrapper::GetHumanReadableName() const {
return "Electron / UtilityProcess";
}
} // namespace api

View file

@ -10,17 +10,17 @@
#include <string>
#include "base/callback_list.h"
#include "base/containers/id_map.h"
#include "base/environment.h"
#include "base/memory/weak_ptr.h"
#include "base/process/process_handle.h"
#include "content/public/browser/service_process_host.h"
#include "gin/weak_cell.h"
#include "gin/wrappable.h"
#include "mojo/public/cpp/bindings/message.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "shell/browser/event_emitter_mixin.h"
#include "shell/browser/net/url_loader_network_observer.h"
#include "shell/common/gin_helper/pinnable.h"
#include "shell/common/gin_helper/wrappable.h"
#include "shell/common/gc_plugin.h"
#include "shell/common/gin_helper/self_keep_alive.h"
#include "shell/services/node/public/mojom/node_service.mojom.h"
#include "v8/include/v8-forward.h"
@ -28,11 +28,6 @@ namespace gin {
class Arguments;
} // namespace gin
namespace gin_helper {
template <typename T>
class Handle;
} // namespace gin_helper
namespace base {
class Process;
} // namespace base
@ -44,8 +39,7 @@ class Connector;
namespace electron::api {
class UtilityProcessWrapper final
: public gin_helper::DeprecatedWrappable<UtilityProcessWrapper>,
public gin_helper::Pinnable<UtilityProcessWrapper>,
: public gin::Wrappable<UtilityProcessWrapper>,
public gin_helper::EventEmitterMixin<UtilityProcessWrapper>,
private mojo::MessageReceiver,
public node::mojom::NodeServiceClient,
@ -54,19 +48,6 @@ class UtilityProcessWrapper final
enum class IOHandle : size_t { STDIN = 0, STDOUT = 1, STDERR = 2 };
enum class IOType { IO_PIPE, IO_INHERIT, IO_IGNORE };
~UtilityProcessWrapper() override;
static gin_helper::Handle<UtilityProcessWrapper> Create(gin::Arguments* args);
static raw_ptr<UtilityProcessWrapper> FromProcessId(base::ProcessId pid);
void Shutdown(uint32_t exit_code);
// gin_helper::Wrappable
static gin::DeprecatedWrapperInfo kWrapperInfo;
gin::ObjectTemplateBuilder GetObjectTemplateBuilder(
v8::Isolate* isolate) override;
const char* GetTypeName() override;
private:
UtilityProcessWrapper(node::mojom::NodeServiceParamsPtr params,
std::u16string display_name,
std::map<IOHandle, IOType> stdio,
@ -75,6 +56,25 @@ class UtilityProcessWrapper final
bool use_plugin_helper,
bool create_network_observer,
bool disclaim_responsibility);
~UtilityProcessWrapper() override;
static UtilityProcessWrapper* Create(gin::Arguments* args);
static UtilityProcessWrapper* FromProcessId(base::ProcessId pid);
void Shutdown(uint32_t exit_code);
// gin::Wrappable
static const gin::WrapperInfo kWrapperInfo;
static const char* GetClassName() { return "UtilityProcess"; }
void Trace(cppgc::Visitor*) const override;
const gin::WrapperInfo* wrapper_info() const override;
const char* GetHumanReadableName() const override;
protected:
gin::ObjectTemplateBuilder GetObjectTemplateBuilder(
v8::Isolate* isolate) override;
private:
void OnServiceProcessLaunch(const base::Process& process);
void CloseConnectorPort();
@ -120,12 +120,17 @@ class UtilityProcessWrapper final
bool create_network_observer_ = false;
std::unique_ptr<mojo::Connector> connector_;
blink::MessagePortDescriptor host_port_;
GC_PLUGIN_IGNORE(
"Context tracking of receiver is not needed in the browser process.")
mojo::Receiver<node::mojom::NodeServiceClient> receiver_{this};
GC_PLUGIN_IGNORE(
"Context tracking of remote is not needed in the browser process.")
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};
gin_helper::SelfKeepAlive<UtilityProcessWrapper> keep_alive_{this};
gin::WeakCellFactory<UtilityProcessWrapper> weak_factory_{this};
};
} // namespace electron::api

View file

@ -603,7 +603,7 @@ void ElectronBrowserMainParts::PostMainMessageLoopRun() {
auto& process = it.GetData().GetProcess();
if (!process.IsValid())
continue;
auto utility_process_wrapper =
auto* utility_process_wrapper =
api::UtilityProcessWrapper::FromProcessId(process.Pid());
if (utility_process_wrapper)
utility_process_wrapper->Shutdown(0 /* exit_code */);

View file

@ -27,6 +27,7 @@ enum ElectronWrappablePointerTag : uint16_t {
kElectronServiceWorkerContext, // electron::api::ServiceWorkerContext
kElectronSession, // electron::api::Session
kElectronTray, // electron::api::Tray
kElectronUtilityProcess, // electron::api::UtilityProcessWrapper
kElectronWebRequest, // electron::api::WebRequest
kLastElectronPointerTag = kElectronWebRequest,
};

View file

@ -427,4 +427,142 @@ describe('cpp heap', () => {
expect(result.noDuplicates).to.equal(true, 'should have exactly one PowerMonitor instance');
});
});
describe('utilityProcess module', () => {
it('should appear in heap snapshot while process is running', async () => {
const { remotely } = await startRemoteControlApp(['--expose-internals']);
const result = await remotely(
async (heap: string, snapshotHelper: string, fixturePath: string) => {
const { utilityProcess } = require('electron');
const { once } = require('node:events');
const { recordState } = require(heap);
const { containsRetainingPath } = require(snapshotHelper);
const child = utilityProcess.fork(fixturePath);
await once(child, 'spawn');
const state = recordState();
const found = containsRetainingPath(state.snapshot, ['C++ Persistent roots', 'Electron / UtilityProcess']);
child.kill();
await once(child, 'exit');
return found;
},
path.join(__dirname, '../../third_party/electron_node/test/common/heap'),
path.join(__dirname, 'lib', 'heapsnapshot-helpers.js'),
path.join(__dirname, 'fixtures/api/utility-process/endless.js')
);
expect(result).to.equal(true);
});
it('should be released from heap snapshot after process exits', async () => {
const { remotely } = await startRemoteControlApp(['--expose-internals', '--js-flags=--expose-gc']);
const result = await remotely(
async (heap: string, snapshotHelper: string, fixturePath: string) => {
const { utilityProcess } = require('electron');
const { once } = require('node:events');
const { recordState } = require(heap);
const { containsRetainingPath } = require(snapshotHelper);
const v8Util = (process as any)._linkedBinding('electron_common_v8_util');
let child: any = utilityProcess.fork(fixturePath);
await once(child, 'exit');
child = null;
for (let i = 0; i < 10; i++) {
await new Promise((resolve) => setTimeout(resolve, 0));
v8Util.requestGarbageCollectionForTesting();
}
const state = recordState();
const found = containsRetainingPath(state.snapshot, ['C++ Persistent roots', 'Electron / UtilityProcess']);
return !found;
},
path.join(__dirname, '../../third_party/electron_node/test/common/heap'),
path.join(__dirname, 'lib', 'heapsnapshot-helpers.js'),
path.join(__dirname, 'fixtures/api/utility-process/empty.js')
);
expect(result).to.equal(true, 'UtilityProcess should be released after exit and GC');
});
it('should survive GC when JS reference is dropped but process is still running', async () => {
const rc = await startRemoteControlApp(['--expose-internals', '--js-flags=--expose-gc']);
const result = await rc.remotely(
async (heap: string, snapshotHelper: string, fixturePath: string) => {
const { utilityProcess, app } = require('electron');
const { once } = require('node:events');
const { recordState } = require(heap);
const { containsRetainingPath } = require(snapshotHelper);
const v8Util = (process as any)._linkedBinding('electron_common_v8_util');
let child: any = utilityProcess.fork(fixturePath);
await once(child, 'spawn');
child = null;
// Force GC — the process should still be alive because
// SelfKeepAlive roots the C++ wrapper.
for (let i = 0; i < 10; i++) {
await new Promise((resolve) => setTimeout(resolve, 0));
v8Util.requestGarbageCollectionForTesting();
}
const state = recordState();
const stillAlive = containsRetainingPath(state.snapshot, [
'C++ Persistent roots',
'Electron / UtilityProcess'
]);
setTimeout(() => app.quit());
return stillAlive;
},
path.join(__dirname, '../../third_party/electron_node/test/common/heap'),
path.join(__dirname, 'lib', 'heapsnapshot-helpers.js'),
path.join(__dirname, 'fixtures/api/utility-process/endless.js')
);
const [code] = await once(rc.process, 'exit');
expect(code).to.equal(0);
expect(result).to.equal(true, 'UtilityProcess should survive GC while process is running');
});
it('should not leak when forking multiple processes', async () => {
const { remotely } = await startRemoteControlApp(['--js-flags=--expose-gc']);
const result = await remotely(
async (fixturePath: string) => {
const { utilityProcess } = require('electron');
const { once } = require('node:events');
const { getCppHeapStatistics } = require('node:v8');
const v8Util = (process as any)._linkedBinding('electron_common_v8_util');
async function forkAndWait() {
const child = utilityProcess.fork(fixturePath);
await once(child, 'exit');
}
async function measure(n: number) {
for (let i = 0; i < n; i++) {
await forkAndWait();
}
for (let i = 0; i < 10; i++) {
await new Promise((resolve) => setTimeout(resolve, 0));
v8Util.requestGarbageCollectionForTesting();
}
return getCppHeapStatistics('brief').used_size_bytes;
}
await measure(5);
const after1 = await measure(10);
const after2 = await measure(10);
return { after1, after2 };
},
path.join(__dirname, 'fixtures/api/utility-process/empty.js')
);
const growth = result.after2 - result.after1;
expect(growth).to.be.at.most(
result.after1 * 0.1,
`C++ heap grew by ${growth} bytes between rounds — likely a leak`
);
});
});
});