feat: service worker preload scripts for improved extensions support (#44411)

* feat: preload scripts for service workers

* feat: service worker IPC

* test: service worker preload scripts and ipc
This commit is contained in:
Sam Maddock 2025-01-31 09:32:45 -05:00 committed by GitHub
parent bc22ee7897
commit 26da3c5d6e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
67 changed files with 2103 additions and 298 deletions

View file

@ -25,6 +25,7 @@
#include "shell/common/gin_helper/promise.h"
#include "shell/common/node_includes.h"
#include "shell/common/world_ids.h"
#include "shell/renderer/preload_realm_context.h"
#include "third_party/blink/public/web/web_blob.h"
#include "third_party/blink/public/web/web_element.h"
#include "third_party/blink/public/web/web_local_frame.h"
@ -765,6 +766,14 @@ v8::MaybeLocal<v8::Context> GetTargetContext(v8::Isolate* isolate,
world_id == WorldIDs::MAIN_WORLD_ID
? frame->MainWorldScriptContext()
: frame->GetScriptContextFromWorldId(isolate, world_id);
} else if (execution_context->IsShadowRealmGlobalScope()) {
if (world_id != WorldIDs::MAIN_WORLD_ID) {
isolate->ThrowException(v8::Exception::Error(gin::StringToV8(
isolate, "Isolated worlds are not supported in preload realms.")));
return maybe_target_context;
}
maybe_target_context =
electron::preload_realm::GetInitiatorContext(source_context);
} else {
NOTREACHED();
}

View file

@ -6,6 +6,7 @@
#include "content/public/renderer/render_frame.h"
#include "content/public/renderer/render_frame_observer.h"
#include "content/public/renderer/worker_thread.h"
#include "gin/dictionary.h"
#include "gin/handle.h"
#include "gin/object_template_builder.h"
@ -14,15 +15,20 @@
#include "shell/common/api/api.mojom.h"
#include "shell/common/gin_converters/blink_converter.h"
#include "shell/common/gin_converters/value_converter.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/error_thrower.h"
#include "shell/common/gin_helper/function_template_extensions.h"
#include "shell/common/gin_helper/promise.h"
#include "shell/common/node_bindings.h"
#include "shell/common/node_includes.h"
#include "shell/common/v8_util.h"
#include "shell/renderer/preload_realm_context.h"
#include "shell/renderer/service_worker_data.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
#include "third_party/blink/public/web/modules/service_worker/web_service_worker_context_proxy.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "third_party/blink/public/web/web_message_port_converter.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h" // nogncheck
using blink::WebLocalFrame;
using content::RenderFrame;
@ -40,50 +46,23 @@ RenderFrame* GetCurrentRenderFrame() {
return RenderFrame::FromWebFrame(frame);
}
class IPCRenderer final : public gin::Wrappable<IPCRenderer>,
private content::RenderFrameObserver {
// Thread identifier for the main renderer thread (as opposed to a service
// worker thread).
inline constexpr int kMainThreadId = 0;
bool IsWorkerThread() {
return content::WorkerThread::GetCurrentId() != kMainThreadId;
}
template <typename T>
class IPCBase : public gin::Wrappable<T> {
public:
static gin::WrapperInfo kWrapperInfo;
static gin::Handle<IPCRenderer> Create(v8::Isolate* isolate) {
return gin::CreateHandle(isolate, new IPCRenderer(isolate));
static gin::Handle<T> Create(v8::Isolate* isolate) {
return gin::CreateHandle(isolate, new T(isolate));
}
explicit IPCRenderer(v8::Isolate* isolate)
: content::RenderFrameObserver(GetCurrentRenderFrame()) {
RenderFrame* render_frame = GetCurrentRenderFrame();
DCHECK(render_frame);
weak_context_ =
v8::Global<v8::Context>(isolate, isolate->GetCurrentContext());
weak_context_.SetWeak();
render_frame->GetRemoteAssociatedInterfaces()->GetInterface(
&electron_ipc_remote_);
}
void OnDestruct() override { electron_ipc_remote_.reset(); }
void WillReleaseScriptContext(v8::Local<v8::Context> context,
int32_t world_id) override {
if (weak_context_.IsEmpty() ||
weak_context_.Get(context->GetIsolate()) == context)
electron_ipc_remote_.reset();
}
// gin::Wrappable:
gin::ObjectTemplateBuilder GetObjectTemplateBuilder(
v8::Isolate* isolate) override {
return gin::Wrappable<IPCRenderer>::GetObjectTemplateBuilder(isolate)
.SetMethod("send", &IPCRenderer::SendMessage)
.SetMethod("sendSync", &IPCRenderer::SendSync)
.SetMethod("sendToHost", &IPCRenderer::SendToHost)
.SetMethod("invoke", &IPCRenderer::Invoke)
.SetMethod("postMessage", &IPCRenderer::PostMessage);
}
const char* GetTypeName() override { return "IPCRenderer"; }
private:
void SendMessage(v8::Isolate* isolate,
gin_helper::ErrorThrower thrower,
bool internal,
@ -202,18 +181,95 @@ class IPCRenderer final : public gin::Wrappable<IPCRenderer>,
return electron::DeserializeV8Value(isolate, result);
}
v8::Global<v8::Context> weak_context_;
// gin::Wrappable:
gin::ObjectTemplateBuilder GetObjectTemplateBuilder(
v8::Isolate* isolate) override {
return gin::Wrappable<T>::GetObjectTemplateBuilder(isolate)
.SetMethod("send", &T::SendMessage)
.SetMethod("sendSync", &T::SendSync)
.SetMethod("sendToHost", &T::SendToHost)
.SetMethod("invoke", &T::Invoke)
.SetMethod("postMessage", &T::PostMessage);
}
protected:
mojo::AssociatedRemote<electron::mojom::ElectronApiIPC> electron_ipc_remote_;
};
gin::WrapperInfo IPCRenderer::kWrapperInfo = {gin::kEmbedderNativeGin};
class IPCRenderFrame : public IPCBase<IPCRenderFrame>,
private content::RenderFrameObserver {
public:
explicit IPCRenderFrame(v8::Isolate* isolate)
: content::RenderFrameObserver(GetCurrentRenderFrame()) {
v8::Local<v8::Context> context = isolate->GetCurrentContext();
blink::ExecutionContext* execution_context =
blink::ExecutionContext::From(context);
if (execution_context->IsWindow()) {
RenderFrame* render_frame = GetCurrentRenderFrame();
DCHECK(render_frame);
render_frame->GetRemoteAssociatedInterfaces()->GetInterface(
&electron_ipc_remote_);
} else {
NOTREACHED();
}
weak_context_ =
v8::Global<v8::Context>(isolate, isolate->GetCurrentContext());
weak_context_.SetWeak();
}
void OnDestruct() override { electron_ipc_remote_.reset(); }
void WillReleaseScriptContext(v8::Local<v8::Context> context,
int32_t world_id) override {
if (weak_context_.IsEmpty() ||
weak_context_.Get(context->GetIsolate()) == context) {
OnDestruct();
}
}
const char* GetTypeName() override { return "IPCRenderFrame"; }
private:
v8::Global<v8::Context> weak_context_;
};
template <>
gin::WrapperInfo IPCBase<IPCRenderFrame>::kWrapperInfo = {
gin::kEmbedderNativeGin};
class IPCServiceWorker : public IPCBase<IPCServiceWorker>,
public content::WorkerThread::Observer {
public:
explicit IPCServiceWorker(v8::Isolate* isolate) {
DCHECK(IsWorkerThread());
content::WorkerThread::AddObserver(this);
electron::ServiceWorkerData* service_worker_data =
electron::preload_realm::GetServiceWorkerData(
isolate->GetCurrentContext());
DCHECK(service_worker_data);
service_worker_data->proxy()->GetRemoteAssociatedInterface(
electron_ipc_remote_.BindNewEndpointAndPassReceiver());
}
void WillStopCurrentWorkerThread() override { electron_ipc_remote_.reset(); }
const char* GetTypeName() override { return "IPCServiceWorker"; }
};
template <>
gin::WrapperInfo IPCBase<IPCServiceWorker>::kWrapperInfo = {
gin::kEmbedderNativeGin};
void Initialize(v8::Local<v8::Object> exports,
v8::Local<v8::Value> unused,
v8::Local<v8::Context> context,
void* priv) {
gin::Dictionary dict(context->GetIsolate(), exports);
dict.Set("ipc", IPCRenderer::Create(context->GetIsolate()));
gin_helper::Dictionary dict(context->GetIsolate(), exports);
dict.SetMethod("createForRenderFrame", &IPCRenderFrame::Create);
dict.SetMethod("createForServiceWorker", &IPCServiceWorker::Create);
}
} // namespace

View file

@ -21,6 +21,7 @@
#include "shell/common/options_switches.h"
#include "shell/common/thread_restrictions.h"
#include "shell/common/v8_util.h"
#include "shell/renderer/electron_ipc_native.h"
#include "shell/renderer/electron_render_frame_observer.h"
#include "shell/renderer/renderer_client_base.h"
#include "third_party/blink/public/mojom/frame/user_activation_notification_type.mojom-shared.h"
@ -31,73 +32,6 @@
namespace electron {
namespace {
constexpr std::string_view kIpcKey = "ipcNative";
// Gets the private object under kIpcKey
v8::Local<v8::Object> GetIpcObject(v8::Local<v8::Context> context) {
auto* isolate = context->GetIsolate();
auto binding_key = gin::StringToV8(isolate, kIpcKey);
auto private_binding_key = v8::Private::ForApi(isolate, binding_key);
auto global_object = context->Global();
auto value =
global_object->GetPrivate(context, private_binding_key).ToLocalChecked();
if (value.IsEmpty() || !value->IsObject()) {
LOG(ERROR) << "Attempted to get the 'ipcNative' object but it was missing";
return {};
}
return value->ToObject(context).ToLocalChecked();
}
void InvokeIpcCallback(v8::Local<v8::Context> context,
const std::string& callback_name,
std::vector<v8::Local<v8::Value>> args) {
TRACE_EVENT0("devtools.timeline", "FunctionCall");
auto* isolate = context->GetIsolate();
auto ipcNative = GetIpcObject(context);
if (ipcNative.IsEmpty())
return;
// Only set up the node::CallbackScope if there's a node environment.
// Sandboxed renderers don't have a node environment.
std::unique_ptr<node::CallbackScope> callback_scope;
if (node::Environment::GetCurrent(context)) {
callback_scope = std::make_unique<node::CallbackScope>(
isolate, ipcNative, node::async_context{0, 0});
}
auto callback_key = gin::ConvertToV8(isolate, callback_name)
->ToString(context)
.ToLocalChecked();
auto callback_value = ipcNative->Get(context, callback_key).ToLocalChecked();
DCHECK(callback_value->IsFunction()); // set by init.ts
auto callback = callback_value.As<v8::Function>();
std::ignore = callback->Call(context, ipcNative, args.size(), args.data());
}
void EmitIPCEvent(v8::Local<v8::Context> context,
bool internal,
const std::string& channel,
std::vector<v8::Local<v8::Value>> ports,
v8::Local<v8::Value> args) {
auto* isolate = context->GetIsolate();
v8::HandleScope handle_scope(isolate);
v8::Context::Scope context_scope(context);
v8::MicrotasksScope script_scope(isolate, context->GetMicrotaskQueue(),
v8::MicrotasksScope::kRunMicrotasks);
std::vector<v8::Local<v8::Value>> argv = {
gin::ConvertToV8(isolate, internal), gin::ConvertToV8(isolate, channel),
gin::ConvertToV8(isolate, ports), args};
InvokeIpcCallback(context, "onMessage", argv);
}
} // namespace
ElectronApiServiceImpl::~ElectronApiServiceImpl() = default;
ElectronApiServiceImpl::ElectronApiServiceImpl(
@ -166,7 +100,7 @@ void ElectronApiServiceImpl::Message(bool internal,
v8::Local<v8::Value> args = gin::ConvertToV8(isolate, arguments);
EmitIPCEvent(context, internal, channel, {}, args);
ipc_native::EmitIPCEvent(context, internal, channel, {}, args);
}
void ElectronApiServiceImpl::ReceivePostMessage(
@ -193,7 +127,8 @@ void ElectronApiServiceImpl::ReceivePostMessage(
std::vector<v8::Local<v8::Value>> args = {message_value};
EmitIPCEvent(context, false, channel, ports, gin::ConvertToV8(isolate, args));
ipc_native::EmitIPCEvent(context, false, channel, ports,
gin::ConvertToV8(isolate, args));
}
void ElectronApiServiceImpl::TakeHeapSnapshot(

View file

@ -0,0 +1,84 @@
// Copyright (c) 2019 Slack Technologies, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "electron/shell/renderer/electron_ipc_native.h"
#include "base/trace_event/trace_event.h"
#include "shell/common/gin_converters/blink_converter.h"
#include "shell/common/gin_converters/value_converter.h"
#include "shell/common/node_includes.h"
#include "shell/common/v8_util.h"
#include "third_party/blink/public/web/blink.h"
#include "third_party/blink/public/web/web_message_port_converter.h"
namespace electron::ipc_native {
namespace {
constexpr std::string_view kIpcKey = "ipcNative";
// Gets the private object under kIpcKey
v8::Local<v8::Object> GetIpcObject(const v8::Local<v8::Context>& context) {
auto* isolate = context->GetIsolate();
auto binding_key = gin::StringToV8(isolate, kIpcKey);
auto private_binding_key = v8::Private::ForApi(isolate, binding_key);
auto global_object = context->Global();
auto value =
global_object->GetPrivate(context, private_binding_key).ToLocalChecked();
if (value.IsEmpty() || !value->IsObject()) {
LOG(ERROR) << "Attempted to get the 'ipcNative' object but it was missing";
return {};
}
return value->ToObject(context).ToLocalChecked();
}
void InvokeIpcCallback(const v8::Local<v8::Context>& context,
const std::string& callback_name,
std::vector<v8::Local<v8::Value>> args) {
TRACE_EVENT0("devtools.timeline", "FunctionCall");
auto* isolate = context->GetIsolate();
auto ipcNative = GetIpcObject(context);
if (ipcNative.IsEmpty())
return;
// Only set up the node::CallbackScope if there's a node environment.
// Sandboxed renderers don't have a node environment.
std::unique_ptr<node::CallbackScope> callback_scope;
if (node::Environment::GetCurrent(context)) {
callback_scope = std::make_unique<node::CallbackScope>(
isolate, ipcNative, node::async_context{0, 0});
}
auto callback_key = gin::ConvertToV8(isolate, callback_name)
->ToString(context)
.ToLocalChecked();
auto callback_value = ipcNative->Get(context, callback_key).ToLocalChecked();
DCHECK(callback_value->IsFunction()); // set by init.ts
auto callback = callback_value.As<v8::Function>();
std::ignore = callback->Call(context, ipcNative, args.size(), args.data());
}
} // namespace
void EmitIPCEvent(const v8::Local<v8::Context>& context,
bool internal,
const std::string& channel,
std::vector<v8::Local<v8::Value>> ports,
v8::Local<v8::Value> args) {
auto* isolate = context->GetIsolate();
v8::HandleScope handle_scope(isolate);
v8::Context::Scope context_scope(context);
v8::MicrotasksScope script_scope(isolate, context->GetMicrotaskQueue(),
v8::MicrotasksScope::kRunMicrotasks);
std::vector<v8::Local<v8::Value>> argv = {
gin::ConvertToV8(isolate, internal), gin::ConvertToV8(isolate, channel),
gin::ConvertToV8(isolate, ports), args};
InvokeIpcCallback(context, "onMessage", argv);
}
} // namespace electron::ipc_native

View file

@ -0,0 +1,22 @@
// Copyright (c) 2019 Slack Technologies, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_RENDERER_ELECTRON_IPC_NATIVE_H_
#define ELECTRON_SHELL_RENDERER_ELECTRON_IPC_NATIVE_H_
#include <vector>
#include "v8/include/v8-forward.h"
namespace electron::ipc_native {
void EmitIPCEvent(const v8::Local<v8::Context>& context,
bool internal,
const std::string& channel,
std::vector<v8::Local<v8::Value>> ports,
v8::Local<v8::Value> args);
} // namespace electron::ipc_native
#endif // ELECTRON_SHELL_RENDERER_ELECTRON_IPC_NATIVE_H_

View file

@ -11,18 +11,19 @@
#include "base/base_paths.h"
#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/process/process_handle.h"
#include "base/process/process_metrics.h"
#include "content/public/renderer/render_frame.h"
#include "shell/common/api/electron_bindings.h"
#include "shell/common/application_info.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/microtasks_scope.h"
#include "shell/common/node_bindings.h"
#include "shell/common/node_includes.h"
#include "shell/common/node_util.h"
#include "shell/common/options_switches.h"
#include "shell/renderer/electron_render_frame_observer.h"
#include "shell/renderer/preload_realm_context.h"
#include "shell/renderer/preload_utils.h"
#include "shell/renderer/service_worker_data.h"
#include "third_party/blink/public/common/web_preferences/web_preferences.h"
#include "third_party/blink/public/platform/scheduler/web_agent_group_scheduler.h"
#include "third_party/blink/public/web/blink.h"
@ -33,67 +34,10 @@ namespace electron {
namespace {
// Data which only lives on the service worker's thread
constinit thread_local ServiceWorkerData* service_worker_data = nullptr;
constexpr std::string_view kEmitProcessEventKey = "emit-process-event";
constexpr std::string_view kBindingCacheKey = "native-binding-cache";
v8::Local<v8::Object> GetBindingCache(v8::Isolate* isolate) {
auto context = isolate->GetCurrentContext();
gin_helper::Dictionary global(isolate, context->Global());
v8::Local<v8::Value> cache;
if (!global.GetHidden(kBindingCacheKey, &cache)) {
cache = v8::Object::New(isolate);
global.SetHidden(kBindingCacheKey, cache);
}
return cache->ToObject(context).ToLocalChecked();
}
// adapted from node.cc
v8::Local<v8::Value> GetBinding(v8::Isolate* isolate,
v8::Local<v8::String> key,
gin_helper::Arguments* margs) {
v8::Local<v8::Object> exports;
std::string binding_key = gin::V8ToString(isolate, key);
gin_helper::Dictionary cache(isolate, GetBindingCache(isolate));
if (cache.Get(binding_key, &exports)) {
return exports;
}
auto* mod = node::binding::get_linked_module(binding_key.c_str());
if (!mod) {
char errmsg[1024];
snprintf(errmsg, sizeof(errmsg), "No such binding: %s",
binding_key.c_str());
margs->ThrowError(errmsg);
return exports;
}
exports = v8::Object::New(isolate);
DCHECK_EQ(mod->nm_register_func, nullptr);
DCHECK_NE(mod->nm_context_register_func, nullptr);
mod->nm_context_register_func(exports, v8::Null(isolate),
isolate->GetCurrentContext(), mod->nm_priv);
cache.Set(binding_key, exports);
return exports;
}
v8::Local<v8::Value> CreatePreloadScript(v8::Isolate* isolate,
v8::Local<v8::String> source) {
auto context = isolate->GetCurrentContext();
auto maybe_script = v8::Script::Compile(context, source);
v8::Local<v8::Script> script;
if (!maybe_script.ToLocal(&script))
return {};
return script->Run(context).ToLocalChecked();
}
double Uptime() {
return (base::Time::Now() - base::Process::Current().CreationTime())
.InSecondsF();
}
void InvokeEmitProcessEvent(v8::Local<v8::Context> context,
const std::string& event_name) {
@ -132,8 +76,8 @@ void ElectronSandboxedRendererClient::InitializeBindings(
content::RenderFrame* render_frame) {
auto* isolate = context->GetIsolate();
gin_helper::Dictionary b(isolate, binding);
b.SetMethod("get", GetBinding);
b.SetMethod("createPreloadScript", CreatePreloadScript);
b.SetMethod("get", preload_utils::GetBinding);
b.SetMethod("createPreloadScript", preload_utils::CreatePreloadScript);
auto process = gin_helper::Dictionary::CreateEmpty(isolate);
b.Set("process", process);
@ -141,7 +85,7 @@ void ElectronSandboxedRendererClient::InitializeBindings(
ElectronBindings::BindProcess(isolate, &process, metrics_.get());
BindProcess(isolate, &process, render_frame);
process.SetMethod("uptime", Uptime);
process.SetMethod("uptime", preload_utils::Uptime);
process.Set("argv", base::CommandLine::ForCurrentProcess()->argv());
process.SetReadOnly("pid", base::GetCurrentProcId());
process.SetReadOnly("sandboxed", true);
@ -231,4 +175,44 @@ void ElectronSandboxedRendererClient::EmitProcessEvent(
InvokeEmitProcessEvent(context, event_name);
}
void ElectronSandboxedRendererClient::WillEvaluateServiceWorkerOnWorkerThread(
blink::WebServiceWorkerContextProxy* context_proxy,
v8::Local<v8::Context> v8_context,
int64_t service_worker_version_id,
const GURL& service_worker_scope,
const GURL& script_url,
const blink::ServiceWorkerToken& service_worker_token) {
RendererClientBase::WillEvaluateServiceWorkerOnWorkerThread(
context_proxy, v8_context, service_worker_version_id,
service_worker_scope, script_url, service_worker_token);
auto* command_line = base::CommandLine::ForCurrentProcess();
if (command_line->HasSwitch(switches::kServiceWorkerPreload)) {
if (!service_worker_data) {
service_worker_data = new ServiceWorkerData(
context_proxy, service_worker_version_id, v8_context);
}
preload_realm::OnCreatePreloadableV8Context(v8_context,
service_worker_data);
}
}
void ElectronSandboxedRendererClient::
WillDestroyServiceWorkerContextOnWorkerThread(
v8::Local<v8::Context> context,
int64_t service_worker_version_id,
const GURL& service_worker_scope,
const GURL& script_url) {
if (service_worker_data) {
DCHECK_EQ(service_worker_version_id,
service_worker_data->service_worker_version_id());
delete service_worker_data;
service_worker_data = nullptr;
}
RendererClientBase::WillDestroyServiceWorkerContextOnWorkerThread(
context, service_worker_version_id, service_worker_scope, script_url);
}
} // namespace electron

View file

@ -42,6 +42,18 @@ class ElectronSandboxedRendererClient : public RendererClientBase {
void RenderFrameCreated(content::RenderFrame*) override;
void RunScriptsAtDocumentStart(content::RenderFrame* render_frame) override;
void RunScriptsAtDocumentEnd(content::RenderFrame* render_frame) override;
void WillEvaluateServiceWorkerOnWorkerThread(
blink::WebServiceWorkerContextProxy* context_proxy,
v8::Local<v8::Context> v8_context,
int64_t service_worker_version_id,
const GURL& service_worker_scope,
const GURL& script_url,
const blink::ServiceWorkerToken& service_worker_token) override;
void WillDestroyServiceWorkerContextOnWorkerThread(
v8::Local<v8::Context> context,
int64_t service_worker_version_id,
const GURL& service_worker_scope,
const GURL& script_url) override;
private:
void EmitProcessEvent(content::RenderFrame* render_frame,

View file

@ -0,0 +1,295 @@
// Copyright (c) 2025 Salesforce, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/renderer/preload_realm_context.h"
#include "base/command_line.h"
#include "base/process/process.h"
#include "base/process/process_metrics.h"
#include "shell/common/api/electron_bindings.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/node_includes.h"
#include "shell/common/node_util.h"
#include "shell/renderer/preload_utils.h"
#include "shell/renderer/service_worker_data.h"
#include "third_party/blink/renderer/bindings/core/v8/script_controller.h" // nogncheck
#include "third_party/blink/renderer/core/execution_context/execution_context.h" // nogncheck
#include "third_party/blink/renderer/core/inspector/worker_thread_debugger.h" // nogncheck
#include "third_party/blink/renderer/core/shadow_realm/shadow_realm_global_scope.h" // nogncheck
#include "third_party/blink/renderer/core/workers/worker_or_worklet_global_scope.h" // nogncheck
#include "third_party/blink/renderer/platform/bindings/script_state.h" // nogncheck
#include "third_party/blink/renderer/platform/bindings/v8_dom_wrapper.h" // nogncheck
#include "third_party/blink/renderer/platform/bindings/v8_per_context_data.h" // nogncheck
#include "third_party/blink/renderer/platform/context_lifecycle_observer.h" // nogncheck
#include "v8/include/v8-context.h"
namespace electron::preload_realm {
namespace {
static constexpr int kElectronContextEmbedderDataIndex =
static_cast<int>(gin::kPerContextDataStartIndex) +
static_cast<int>(gin::kEmbedderElectron);
// This is a helper class to make the initiator ExecutionContext the owner
// of a ShadowRealmGlobalScope and its ScriptState. When the initiator
// ExecutionContext is destroyed, the ShadowRealmGlobalScope is destroyed,
// too.
class PreloadRealmLifetimeController
: public blink::GarbageCollected<PreloadRealmLifetimeController>,
public blink::ContextLifecycleObserver {
public:
explicit PreloadRealmLifetimeController(
blink::ExecutionContext* initiator_execution_context,
blink::ScriptState* initiator_script_state,
blink::ShadowRealmGlobalScope* shadow_realm_global_scope,
blink::ScriptState* shadow_realm_script_state,
electron::ServiceWorkerData* service_worker_data)
: initiator_script_state_(initiator_script_state),
is_initiator_worker_or_worklet_(
initiator_execution_context->IsWorkerOrWorkletGlobalScope()),
shadow_realm_global_scope_(shadow_realm_global_scope),
shadow_realm_script_state_(shadow_realm_script_state),
service_worker_data_(service_worker_data) {
// Align lifetime of this controller to that of the initiator's context.
self_ = this;
SetContextLifecycleNotifier(initiator_execution_context);
RegisterDebugger(initiator_execution_context);
initiator_context()->SetAlignedPointerInEmbedderData(
kElectronContextEmbedderDataIndex, static_cast<void*>(this));
realm_context()->SetAlignedPointerInEmbedderData(
kElectronContextEmbedderDataIndex, static_cast<void*>(this));
metrics_ = base::ProcessMetrics::CreateCurrentProcessMetrics();
RunInitScript();
}
static PreloadRealmLifetimeController* From(v8::Local<v8::Context> context) {
if (context->GetNumberOfEmbedderDataFields() <=
kElectronContextEmbedderDataIndex) {
return nullptr;
}
auto* controller = static_cast<PreloadRealmLifetimeController*>(
context->GetAlignedPointerFromEmbedderData(
kElectronContextEmbedderDataIndex));
CHECK(controller);
return controller;
}
void Trace(blink::Visitor* visitor) const override {
visitor->Trace(initiator_script_state_);
visitor->Trace(shadow_realm_global_scope_);
visitor->Trace(shadow_realm_script_state_);
ContextLifecycleObserver::Trace(visitor);
}
v8::MaybeLocal<v8::Context> GetContext() {
return shadow_realm_script_state_->ContextIsValid()
? shadow_realm_script_state_->GetContext()
: v8::MaybeLocal<v8::Context>();
}
v8::MaybeLocal<v8::Context> GetInitiatorContext() {
return initiator_script_state_->ContextIsValid()
? initiator_script_state_->GetContext()
: v8::MaybeLocal<v8::Context>();
}
electron::ServiceWorkerData* service_worker_data() {
return service_worker_data_;
}
protected:
void ContextDestroyed() override {
v8::HandleScope handle_scope(realm_isolate());
realm_context()->SetAlignedPointerInEmbedderData(
kElectronContextEmbedderDataIndex, nullptr);
// See ShadowRealmGlobalScope::ContextDestroyed
shadow_realm_script_state_->DisposePerContextData();
if (is_initiator_worker_or_worklet_) {
shadow_realm_script_state_->DissociateContext();
}
shadow_realm_script_state_.Clear();
shadow_realm_global_scope_->NotifyContextDestroyed();
shadow_realm_global_scope_.Clear();
self_.Clear();
}
private:
v8::Isolate* realm_isolate() {
return shadow_realm_script_state_->GetIsolate();
}
v8::Local<v8::Context> realm_context() {
return shadow_realm_script_state_->GetContext();
}
v8::Local<v8::Context> initiator_context() {
return initiator_script_state_->GetContext();
}
void RegisterDebugger(blink::ExecutionContext* initiator_execution_context) {
v8::Isolate* isolate = realm_isolate();
v8::Local<v8::Context> context = realm_context();
blink::WorkerThreadDebugger* debugger =
blink::WorkerThreadDebugger::From(isolate);
;
const auto* worker_context =
To<blink::WorkerOrWorkletGlobalScope>(initiator_execution_context);
// Override path to make preload realm easier to find in debugger.
blink::KURL url_for_debugger(worker_context->Url());
url_for_debugger.SetPath("electron-preload-realm");
debugger->ContextCreated(worker_context->GetThread(), url_for_debugger,
context);
}
void RunInitScript() {
v8::Isolate* isolate = realm_isolate();
v8::Local<v8::Context> context = realm_context();
v8::Context::Scope context_scope(context);
v8::MicrotasksScope microtasks_scope(
isolate, context->GetMicrotaskQueue(),
v8::MicrotasksScope::kDoNotRunMicrotasks);
v8::Local<v8::Object> binding = v8::Object::New(isolate);
gin_helper::Dictionary b(isolate, binding);
b.SetMethod("get", preload_utils::GetBinding);
b.SetMethod("createPreloadScript", preload_utils::CreatePreloadScript);
gin_helper::Dictionary process = gin::Dictionary::CreateEmpty(isolate);
b.Set("process", process);
ElectronBindings::BindProcess(isolate, &process, metrics_.get());
process.SetMethod("uptime", preload_utils::Uptime);
process.Set("argv", base::CommandLine::ForCurrentProcess()->argv());
process.SetReadOnly("pid", base::GetCurrentProcId());
process.SetReadOnly("sandboxed", true);
process.SetReadOnly("type", "service-worker");
process.SetReadOnly("contextIsolated", true);
std::vector<v8::Local<v8::String>> preload_realm_bundle_params = {
node::FIXED_ONE_BYTE_STRING(isolate, "binding")};
std::vector<v8::Local<v8::Value>> preload_realm_bundle_args = {binding};
util::CompileAndCall(context, "electron/js2c/preload_realm_bundle",
&preload_realm_bundle_params,
&preload_realm_bundle_args);
}
const blink::WeakMember<blink::ScriptState> initiator_script_state_;
bool is_initiator_worker_or_worklet_;
blink::Member<blink::ShadowRealmGlobalScope> shadow_realm_global_scope_;
blink::Member<blink::ScriptState> shadow_realm_script_state_;
std::unique_ptr<base::ProcessMetrics> metrics_;
raw_ptr<ServiceWorkerData> service_worker_data_;
blink::Persistent<PreloadRealmLifetimeController> self_;
};
} // namespace
v8::MaybeLocal<v8::Context> GetInitiatorContext(
v8::Local<v8::Context> context) {
DCHECK(!context.IsEmpty());
blink::ExecutionContext* execution_context =
blink::ExecutionContext::From(context);
if (!execution_context->IsShadowRealmGlobalScope())
return v8::MaybeLocal<v8::Context>();
auto* controller = PreloadRealmLifetimeController::From(context);
if (controller)
return controller->GetInitiatorContext();
return v8::MaybeLocal<v8::Context>();
}
v8::MaybeLocal<v8::Context> GetPreloadRealmContext(
v8::Local<v8::Context> context) {
DCHECK(!context.IsEmpty());
blink::ExecutionContext* execution_context =
blink::ExecutionContext::From(context);
if (!execution_context->IsServiceWorkerGlobalScope())
return v8::MaybeLocal<v8::Context>();
auto* controller = PreloadRealmLifetimeController::From(context);
if (controller)
return controller->GetContext();
return v8::MaybeLocal<v8::Context>();
}
electron::ServiceWorkerData* GetServiceWorkerData(
v8::Local<v8::Context> context) {
auto* controller = PreloadRealmLifetimeController::From(context);
return controller ? controller->service_worker_data() : nullptr;
}
void OnCreatePreloadableV8Context(
v8::Local<v8::Context> initiator_context,
electron::ServiceWorkerData* service_worker_data) {
v8::Isolate* isolate = initiator_context->GetIsolate();
blink::ScriptState* initiator_script_state =
blink::ScriptState::MaybeFrom(isolate, initiator_context);
DCHECK(initiator_script_state);
blink::ExecutionContext* initiator_execution_context =
blink::ExecutionContext::From(initiator_context);
DCHECK(initiator_execution_context);
blink::DOMWrapperWorld* world = blink::DOMWrapperWorld::Create(
isolate, blink::DOMWrapperWorld::WorldType::kShadowRealm);
CHECK(world); // Not yet run out of the world id.
// Create a new ShadowRealmGlobalScope.
blink::ShadowRealmGlobalScope* shadow_realm_global_scope =
blink::MakeGarbageCollected<blink::ShadowRealmGlobalScope>(
initiator_execution_context);
const blink::WrapperTypeInfo* wrapper_type_info =
shadow_realm_global_scope->GetWrapperTypeInfo();
// Create a new v8::Context.
// Initialize V8 extensions before creating the context.
v8::ExtensionConfiguration extension_configuration =
blink::ScriptController::ExtensionsFor(shadow_realm_global_scope);
v8::Local<v8::ObjectTemplate> global_template =
wrapper_type_info->GetV8ClassTemplate(isolate, *world)
.As<v8::FunctionTemplate>()
->InstanceTemplate();
v8::Local<v8::Object> global_proxy; // Will request a new global proxy.
v8::Local<v8::Context> context =
v8::Context::New(isolate, &extension_configuration, global_template,
global_proxy, v8::DeserializeInternalFieldsCallback(),
initiator_execution_context->GetMicrotaskQueue());
context->UseDefaultSecurityToken();
// Associate the Blink object with the v8::Context.
blink::ScriptState* script_state =
blink::ScriptState::Create(context, world, shadow_realm_global_scope);
// Associate the Blink object with the v8::Objects.
global_proxy = context->Global();
blink::V8DOMWrapper::SetNativeInfo(isolate, global_proxy,
shadow_realm_global_scope);
v8::Local<v8::Object> global_object =
global_proxy->GetPrototype().As<v8::Object>();
blink::V8DOMWrapper::SetNativeInfo(isolate, global_object,
shadow_realm_global_scope);
// Install context-dependent properties.
std::ignore =
script_state->PerContextData()->ConstructorForType(wrapper_type_info);
// Make the initiator execution context the owner of the
// ShadowRealmGlobalScope and the ScriptState.
blink::MakeGarbageCollected<PreloadRealmLifetimeController>(
initiator_execution_context, initiator_script_state,
shadow_realm_global_scope, script_state, service_worker_data);
}
} // namespace electron::preload_realm

View file

@ -0,0 +1,34 @@
// Copyright (c) 2025 Salesforce, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_RENDERER_PRELOAD_REALM_CONTEXT_H_
#define ELECTRON_SHELL_RENDERER_PRELOAD_REALM_CONTEXT_H_
#include "v8/include/v8-forward.h"
namespace electron {
class ServiceWorkerData;
}
namespace electron::preload_realm {
// Get initiator context given the preload context.
v8::MaybeLocal<v8::Context> GetInitiatorContext(v8::Local<v8::Context> context);
// Get the preload context given the initiator context.
v8::MaybeLocal<v8::Context> GetPreloadRealmContext(
v8::Local<v8::Context> context);
// Get service worker data given the preload realm context.
electron::ServiceWorkerData* GetServiceWorkerData(
v8::Local<v8::Context> context);
// Create
void OnCreatePreloadableV8Context(
v8::Local<v8::Context> initiator_context,
electron::ServiceWorkerData* service_worker_data);
} // namespace electron::preload_realm
#endif // ELECTRON_SHELL_RENDERER_PRELOAD_REALM_CONTEXT_H_

View file

@ -0,0 +1,80 @@
// Copyright (c) 2016 GitHub, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/renderer/preload_utils.h"
#include "base/process/process.h"
#include "shell/common/gin_helper/arguments.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/node_includes.h"
#include "v8/include/v8-context.h"
namespace electron::preload_utils {
namespace {
constexpr std::string_view kBindingCacheKey = "native-binding-cache";
v8::Local<v8::Object> GetBindingCache(v8::Isolate* isolate) {
auto context = isolate->GetCurrentContext();
gin_helper::Dictionary global(isolate, context->Global());
v8::Local<v8::Value> cache;
if (!global.GetHidden(kBindingCacheKey, &cache)) {
cache = v8::Object::New(isolate);
global.SetHidden(kBindingCacheKey, cache);
}
return cache->ToObject(context).ToLocalChecked();
}
} // namespace
// adapted from node.cc
v8::Local<v8::Value> GetBinding(v8::Isolate* isolate,
v8::Local<v8::String> key,
gin_helper::Arguments* margs) {
v8::Local<v8::Object> exports;
std::string binding_key = gin::V8ToString(isolate, key);
gin_helper::Dictionary cache(isolate, GetBindingCache(isolate));
if (cache.Get(binding_key, &exports)) {
return exports;
}
auto* mod = node::binding::get_linked_module(binding_key.c_str());
if (!mod) {
char errmsg[1024];
snprintf(errmsg, sizeof(errmsg), "No such binding: %s",
binding_key.c_str());
margs->ThrowError(errmsg);
return exports;
}
exports = v8::Object::New(isolate);
DCHECK_EQ(mod->nm_register_func, nullptr);
DCHECK_NE(mod->nm_context_register_func, nullptr);
mod->nm_context_register_func(exports, v8::Null(isolate),
isolate->GetCurrentContext(), mod->nm_priv);
cache.Set(binding_key, exports);
return exports;
}
v8::Local<v8::Value> CreatePreloadScript(v8::Isolate* isolate,
v8::Local<v8::String> source) {
auto context = isolate->GetCurrentContext();
auto maybe_script = v8::Script::Compile(context, source);
v8::Local<v8::Script> script;
if (!maybe_script.ToLocal(&script))
return {};
return script->Run(context).ToLocalChecked();
}
double Uptime() {
return (base::Time::Now() - base::Process::Current().CreationTime())
.InSecondsF();
}
} // namespace electron::preload_utils

View file

@ -0,0 +1,27 @@
// Copyright (c) 2016 GitHub, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_RENDERER_PRELOAD_UTILS_H_
#define ELECTRON_SHELL_RENDERER_PRELOAD_UTILS_H_
#include "v8/include/v8-forward.h"
namespace gin_helper {
class Arguments;
}
namespace electron::preload_utils {
v8::Local<v8::Value> GetBinding(v8::Isolate* isolate,
v8::Local<v8::String> key,
gin_helper::Arguments* margs);
v8::Local<v8::Value> CreatePreloadScript(v8::Isolate* isolate,
v8::Local<v8::String> source);
double Uptime();
} // namespace electron::preload_utils
#endif // ELECTRON_SHELL_RENDERER_PRELOAD_UTILS_H_

View file

@ -0,0 +1,72 @@
// Copyright (c) 2025 Salesforce, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "electron/shell/renderer/service_worker_data.h"
#include "shell/common/gin_converters/blink_converter.h"
#include "shell/common/gin_converters/value_converter.h"
#include "shell/common/heap_snapshot.h"
#include "shell/renderer/electron_ipc_native.h"
#include "shell/renderer/preload_realm_context.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_registry.h"
namespace electron {
ServiceWorkerData::~ServiceWorkerData() = default;
ServiceWorkerData::ServiceWorkerData(blink::WebServiceWorkerContextProxy* proxy,
int64_t service_worker_version_id,
const v8::Local<v8::Context>& v8_context)
: proxy_(proxy),
service_worker_version_id_(service_worker_version_id),
isolate_(v8_context->GetIsolate()),
v8_context_(v8_context->GetIsolate(), v8_context) {
proxy_->GetAssociatedInterfaceRegistry()
.AddInterface<mojom::ElectronRenderer>(
base::BindRepeating(&ServiceWorkerData::OnElectronRendererRequest,
weak_ptr_factory_.GetWeakPtr()));
}
void ServiceWorkerData::OnElectronRendererRequest(
mojo::PendingAssociatedReceiver<mojom::ElectronRenderer> receiver) {
receiver_.reset();
receiver_.Bind(std::move(receiver));
}
void ServiceWorkerData::Message(bool internal,
const std::string& channel,
blink::CloneableMessage arguments) {
v8::Isolate* isolate = isolate_.get();
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Context> context = v8_context_.Get(isolate_);
v8::MaybeLocal<v8::Context> maybe_preload_context =
preload_realm::GetPreloadRealmContext(context);
if (maybe_preload_context.IsEmpty()) {
return;
}
v8::Local<v8::Context> preload_context =
maybe_preload_context.ToLocalChecked();
v8::Context::Scope context_scope(preload_context);
v8::Local<v8::Value> args = gin::ConvertToV8(isolate, arguments);
ipc_native::EmitIPCEvent(preload_context, internal, channel, {}, args);
}
void ServiceWorkerData::ReceivePostMessage(const std::string& channel,
blink::TransferableMessage message) {
NOTIMPLEMENTED();
}
void ServiceWorkerData::TakeHeapSnapshot(mojo::ScopedHandle file,
TakeHeapSnapshotCallback callback) {
NOTIMPLEMENTED();
std::move(callback).Run(false);
}
} // namespace electron

View file

@ -0,0 +1,68 @@
// Copyright (c) 2025 Salesforce, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_RENDERER_SERVICE_WORKER_DATA_H_
#define ELECTRON_SHELL_RENDERER_SERVICE_WORKER_DATA_H_
#include <string>
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "electron/shell/common/api/api.mojom.h"
#include "mojo/public/cpp/bindings/associated_receiver.h"
#include "mojo/public/cpp/bindings/associated_remote.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "third_party/blink/public/web/modules/service_worker/web_service_worker_context_proxy.h"
#include "v8/include/v8-context.h"
#include "v8/include/v8-forward.h"
namespace electron {
// Per ServiceWorker data in worker thread.
class ServiceWorkerData : public mojom::ElectronRenderer {
public:
ServiceWorkerData(blink::WebServiceWorkerContextProxy* proxy,
int64_t service_worker_version_id,
const v8::Local<v8::Context>& v8_context);
~ServiceWorkerData() override;
// disable copy
ServiceWorkerData(const ServiceWorkerData&) = delete;
ServiceWorkerData& operator=(const ServiceWorkerData&) = delete;
int64_t service_worker_version_id() const {
return service_worker_version_id_;
}
blink::WebServiceWorkerContextProxy* proxy() const { return proxy_; }
// mojom::ElectronRenderer
void Message(bool internal,
const std::string& channel,
blink::CloneableMessage arguments) override;
void ReceivePostMessage(const std::string& channel,
blink::TransferableMessage message) override;
void TakeHeapSnapshot(mojo::ScopedHandle file,
TakeHeapSnapshotCallback callback) override;
private:
void OnElectronRendererRequest(
mojo::PendingAssociatedReceiver<mojom::ElectronRenderer> receiver);
raw_ptr<blink::WebServiceWorkerContextProxy> proxy_;
const int64_t service_worker_version_id_;
// The v8 context the bindings are accessible to.
raw_ptr<v8::Isolate> isolate_;
v8::Global<v8::Context> v8_context_;
mojo::AssociatedReceiver<mojom::ElectronRenderer> receiver_{this};
base::WeakPtrFactory<ServiceWorkerData> weak_ptr_factory_{this};
};
} // namespace electron
#endif // ELECTRON_SHELL_RENDERER_SERVICE_WORKER_DATA_H_