Compare commits

...

4 Commits

Author SHA1 Message Date
Shelley Vohr
d85aee8251 fix: stop embed thread before destroying environments in worker teardown
FreeEnvironment (called via environments_.clear()) runs uv_run to drain
handle close callbacks. On Windows, both that uv_run and the embed
thread's PollEvents call GetQueuedCompletionStatus on the same IOCP
handle. IOCP completions are consumed by exactly one waiter, so the
embed thread can steal completions that FreeEnvironment needs, causing
uv_run to block indefinitely. On Linux/Mac epoll_wait/kevent can wake
multiple waiters for the same event so the race doesn't manifest.

Add NodeBindings::StopPolling() which cleanly joins the embed thread
without destroying handles or the loop, and allows PrepareEmbedThread +
StartPolling to restart it later. Call StopPolling() in
WebWorkerObserver::ContextWillDestroy before environments_.clear() so
FreeEnvironment's uv_run is the only thread touching the IOCP.

Split PrepareEmbedThread's handle initialization (uv_async_init,
uv_sem_init) from thread creation via a new embed_thread_prepared_ flag
so the handles survive across stop/restart cycles for pooled worklets
while the embed thread itself can be recreated.
2026-04-09 10:48:11 +02:00
Shelley Vohr
f39ba782d2 chore: address review feedback 2026-04-09 10:48:11 +02:00
Shelley Vohr
7273708ab4 fix: deadlock on Windows when destroying non-AudioWorklet worker contexts
The previous change kept the WebWorkerObserver alive across
ContextWillDestroy so the worker thread could be reused for the next
context (AudioWorklet thread pooling, Chromium CL:5270028). This is
correct for AudioWorklet but wrong for PaintWorklet and other worker
types, which Blink does not pool — each teardown destroys the thread.

For those worker types, ~NodeBindings was deferred to the thread-exit
TLS callback. By that point set_uv_env(nullptr) had already run, so on
Windows the embed thread was parked in GetQueuedCompletionStatus with a
stale async_sent latch that swallowed the eventual WakeupEmbedThread()
from ~NodeBindings. uv_thread_join then blocked forever, deadlocking
renderer navigation. The worker-multiple-destroy crash case timed out
on win-x64/x86/arm64 as a result. macOS/Linux (epoll/kqueue) don't have
the latch and were unaffected.

Plumb is_audio_worklet from WillDestroyWorkerContextOnWorkerThread into
ContextWillDestroy. For non-AudioWorklet contexts, restore the
pre-existing behavior of calling lazy_tls->Set(nullptr) at the end of
the last-context cleanup so ~NodeBindings runs while the worker thread
is still healthy. AudioWorklet continues to keep the observer alive so
the next pooled context can share NodeBindings.
2026-04-09 10:48:11 +02:00
Shelley Vohr
2b633ebe40 fix: nodeIntegrationInWorker not working in AudioWorklet 2026-04-09 10:48:11 +02:00
9 changed files with 472 additions and 35 deletions

View File

@@ -34,6 +34,48 @@ index 7ea6daec53a497bf867d799e041bf6ae7191ef7b..15940624940d5c629c40319f45c59282
agent_group_scheduler_compositor_task_runner =
execution_context->GetScheduler()
->ToFrameScheduler()
diff --git a/third_party/blink/renderer/core/workers/threaded_worklet_messaging_proxy.cc b/third_party/blink/renderer/core/workers/threaded_worklet_messaging_proxy.cc
index 936f5ebe28caa993ed5de0f7de3613fa338e263f..961ac8091aa82128e1cfb8800a7efcb80d100a05 100644
--- a/third_party/blink/renderer/core/workers/threaded_worklet_messaging_proxy.cc
+++ b/third_party/blink/renderer/core/workers/threaded_worklet_messaging_proxy.cc
@@ -13,10 +13,12 @@
#include "third_party/blink/public/platform/task_type.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/execution_context/security_context.h"
+#include "third_party/blink/renderer/core/exported/web_view_impl.h"
#include "third_party/blink/renderer/core/frame/csp/content_security_policy.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/local_frame_client.h"
+#include "third_party/blink/renderer/core/frame/web_local_frame_impl.h"
#include "third_party/blink/renderer/core/inspector/thread_debugger_common_impl.h"
#include "third_party/blink/renderer/core/loader/worker_fetch_context.h"
#include "third_party/blink/renderer/core/origin_trials/origin_trial_context.h"
@@ -135,6 +137,14 @@ void ThreadedWorkletMessagingProxy::Initialize(
DCHECK(csp);
LocalFrameClient* frame_client = window->GetFrame()->Client();
+ auto worklet_settings =
+ std::make_unique<WorkerSettings>(window->GetFrame()->GetSettings());
+ if (auto* web_local_frame = WebLocalFrameImpl::FromFrame(window->GetFrame())) {
+ if (auto* web_view = web_local_frame->ViewImpl()) {
+ worklet_settings->SetNodeIntegrationInWorker(
+ web_view->GetWebPreferences().node_integration_in_worker);
+ }
+ }
auto global_scope_creation_params =
std::make_unique<GlobalScopeCreationParams>(
window->Url(), mojom::blink::ScriptType::kModule, global_scope_name,
@@ -147,8 +157,7 @@ void ThreadedWorkletMessagingProxy::Initialize(
window->GetHttpsState(), worker_clients,
frame_client->CreateWorkerContentSettingsClient(),
OriginTrialContext::GetInheritedTrialFeatures(window).get(),
- base::UnguessableToken::Create(),
- std::make_unique<WorkerSettings>(window->GetFrame()->GetSettings()),
+ base::UnguessableToken::Create(), std::move(worklet_settings),
mojom::blink::V8CacheOptions::kDefault, module_responses_map,
mojo::NullRemote() /* browser_interface_broker */,
window->GetFrame()->Loader().CreateWorkerCodeCacheHost(),
diff --git a/third_party/blink/renderer/core/workers/worker_settings.cc b/third_party/blink/renderer/core/workers/worker_settings.cc
index 45680c5f6ea0c7e89ccf43eb88f8a11e3318c02e..3fa3af62f4e7ba8186441c5e3184b1c04fe32d12 100644
--- a/third_party/blink/renderer/core/workers/worker_settings.cc
@@ -71,3 +113,56 @@ index 45c60dd2c44b05fdd279f759069383479823c7f2..33a2a0337efb9a46293e11d0d09b3fc1
GenericFontFamilySettings generic_font_family_settings_;
};
diff --git a/third_party/blink/renderer/core/workers/worklet_global_scope.cc b/third_party/blink/renderer/core/workers/worklet_global_scope.cc
index b5300dea97f20d72a807543a6da0baf61d21955f..a7030c1ba6851b26c765c7b05cd26e1453866719 100644
--- a/third_party/blink/renderer/core/workers/worklet_global_scope.cc
+++ b/third_party/blink/renderer/core/workers/worklet_global_scope.cc
@@ -32,6 +32,7 @@
#include "third_party/blink/renderer/core/script/modulator.h"
#include "third_party/blink/renderer/core/workers/global_scope_creation_params.h"
#include "third_party/blink/renderer/core/workers/worker_reporting_proxy.h"
+#include "third_party/blink/renderer/core/workers/worker_settings.h"
#include "third_party/blink/renderer/core/workers/worker_thread.h"
#include "third_party/blink/renderer/core/workers/worklet_module_responses_map.h"
#include "third_party/blink/renderer/core/workers/worklet_module_tree_client.h"
@@ -110,6 +111,10 @@ WorkletGlobalScope::WorkletGlobalScope(
parent_cross_origin_isolated_capability_(
creation_params->cross_origin_isolated_capability),
parent_is_isolated_context_(creation_params->parent_is_isolated_context),
+ node_integration_in_worker_(
+ creation_params->worker_settings
+ ? creation_params->worker_settings->NodeIntegrationInWorker()
+ : false),
browser_interface_broker_proxy_(this) {
DCHECK((thread_type_ == ThreadType::kMainThread && frame_) ||
(thread_type_ == ThreadType::kOffMainThread && worker_thread_));
diff --git a/third_party/blink/renderer/core/workers/worklet_global_scope.h b/third_party/blink/renderer/core/workers/worklet_global_scope.h
index c7dd62900f0de48ab992a7c99058f5b6d98212cf..47ceea11ec9db6b67cef6945d165f46c868f4ca5 100644
--- a/third_party/blink/renderer/core/workers/worklet_global_scope.h
+++ b/third_party/blink/renderer/core/workers/worklet_global_scope.h
@@ -140,6 +140,13 @@ class CORE_EXPORT WorkletGlobalScope : public WorkerOrWorkletGlobalScope {
// Returns the WorkletToken that uniquely identifies this worklet.
virtual WorkletToken GetWorkletToken() const = 0;
+ // Electron: returns whether the creator frame had the
+ // `nodeIntegrationInWorker` web preference enabled. Copied from
+ // GlobalScopeCreationParams::worker_settings at construction time so the
+ // value is readable on the worker thread without crossing back to the
+ // main thread.
+ bool NodeIntegrationInWorker() const { return node_integration_in_worker_; }
+
// Returns the ExecutionContextToken that uniquely identifies the parent
// context that created this worklet. Note that this will always be a
// LocalFrameToken.
@@ -207,6 +214,11 @@ class CORE_EXPORT WorkletGlobalScope : public WorkerOrWorkletGlobalScope {
// TODO(crbug.com/1206150): We need a spec for this capability.
const bool parent_is_isolated_context_;
+ // Electron: snapshot of the creator frame's nodeIntegrationInWorker
+ // WebPreference, copied out of GlobalScopeCreationParams::worker_settings
+ // at construction time.
+ const bool node_integration_in_worker_;
+
// This is the interface that handles generated code cache
// requests both to fetch code cache when loading resources
// and to store generated code cache to disk.

View File

@@ -529,14 +529,7 @@ NodeBindings::NodeBindings(BrowserEnvironment browser_env)
uv_loop_{InitEventLoop(browser_env, &worker_loop_)} {}
NodeBindings::~NodeBindings() {
// Quit the embed thread.
embed_closed_ = true;
uv_sem_post(&embed_sem_);
WakeupEmbedThread();
// Wait for everything to be done.
uv_thread_join(&embed_thread_);
StopPolling();
// Clear uv.
uv_sem_destroy(&embed_sem_);
@@ -547,6 +540,26 @@ NodeBindings::~NodeBindings() {
stop_and_close_uv_loop(uv_loop_);
}
void NodeBindings::StopPolling() {
if (!initialized_)
return;
// Tell the embed thread to quit.
embed_closed_ = true;
// The embed thread alternates between uv_sem_wait (waiting for UvRunOnce
// to finish) and PollEvents (waiting for I/O). Wake it from both.
uv_sem_post(&embed_sem_);
WakeupEmbedThread();
// Wait for it to exit.
uv_thread_join(&embed_thread_);
// Allow PrepareEmbedThread + StartPolling to restart.
embed_closed_ = false;
initialized_ = false;
}
node::IsolateData* NodeBindings::isolate_data(
v8::Local<v8::Context> context) const {
if (context->GetNumberOfEmbedderDataFields() <=
@@ -933,12 +946,21 @@ void NodeBindings::PrepareEmbedThread() {
if (initialized_)
return;
// Add dummy handle for libuv, otherwise libuv would quit when there is
// nothing to do.
uv_async_init(uv_loop_, dummy_uv_handle_.get(), nullptr);
// The async handle and semaphore live for the lifetime of this
// NodeBindings instance (destroyed in ~NodeBindings), but the embed
// thread itself may be stopped and restarted via StopPolling /
// PrepareEmbedThread for pooled worklet contexts. Only init the
// handles once.
if (!embed_thread_prepared_) {
// Add dummy handle for libuv, otherwise libuv would quit when there is
// nothing to do.
uv_async_init(uv_loop_, dummy_uv_handle_.get(), nullptr);
// Start worker that will interrupt main loop when having uv events.
uv_sem_init(&embed_sem_, 0);
embed_thread_prepared_ = true;
}
// Start worker that will interrupt main loop when having uv events.
uv_sem_init(&embed_sem_, 0);
uv_thread_create(&embed_thread_, EmbedThreadRunner, this);
}

View File

@@ -157,6 +157,12 @@ class NodeBindings {
// Notify embed thread to start polling after environment is loaded.
void StartPolling();
// Stop the embed thread and polling without destroying handles or the loop.
// After this call, PrepareEmbedThread + StartPolling can restart them.
// Used by pooled worklets that need to pause the embed thread during
// environment teardown but reuse the same NodeBindings for the next context.
void StopPolling();
node::IsolateData* isolate_data(v8::Local<v8::Context> context) const;
// Gets/sets the environment to wrap uv loop.
@@ -225,6 +231,11 @@ class NodeBindings {
// Indicates whether polling thread has been created.
bool initialized_ = false;
// Whether PrepareEmbedThread has initialized the semaphore and async handle.
// Unlike |initialized_|, this is never reset — the handles live until the
// destructor.
bool embed_thread_prepared_ = false;
// Indicates whether the app code has finished loading
// for ESM this is async after the module is loaded
bool app_code_loaded_ = false;

View File

@@ -25,6 +25,7 @@
#include "third_party/blink/renderer/core/frame/web_local_frame_impl.h" // nogncheck
#include "third_party/blink/renderer/core/workers/worker_global_scope.h" // nogncheck
#include "third_party/blink/renderer/core/workers/worker_settings.h" // nogncheck
#include "third_party/blink/renderer/core/workers/worklet_global_scope.h" // nogncheck
namespace electron {
@@ -206,11 +207,20 @@ bool WorkerHasNodeIntegration(blink::ExecutionContext* ec) {
// owing to an inability to customize sandbox policies in these workers
// given that they're run out-of-process.
// Also avoid creating a Node.js environment for worklet global scope
// created on the main thread.
// created on the main thread — those share the page's V8 context where
// Node is already wired up.
if (ec->IsServiceWorkerGlobalScope() || ec->IsSharedWorkerGlobalScope() ||
ec->IsMainThreadWorkletGlobalScope())
return false;
// Off-main-thread worklets (AudioWorklet, PaintWorklet, AnimationWorklet,
// SharedStorageWorklet) have their own dedicated worker thread but do not
// derive from WorkerGlobalScope, so check for them separately and read the
// flag from WorkletGlobalScope, which copies it out of the same
// WorkerSettings as dedicated workers do.
if (auto* wlgs = blink::DynamicTo<blink::WorkletGlobalScope>(ec))
return wlgs->NodeIntegrationInWorker();
auto* wgs = blink::DynamicTo<blink::WorkerGlobalScope>(ec);
if (!wgs)
return false;
@@ -233,9 +243,9 @@ void ElectronRendererClient::WorkerScriptReadyForEvaluationOnWorkerThread(
return;
auto* current = WebWorkerObserver::GetCurrent();
if (current)
return;
WebWorkerObserver::Create()->WorkerScriptReadyForEvaluation(context);
if (!current)
current = WebWorkerObserver::Create();
current->WorkerScriptReadyForEvaluation(context);
}
void ElectronRendererClient::WillDestroyWorkerContextOnWorkerThread(

View File

@@ -10,11 +10,12 @@
#include "base/no_destructor.h"
#include "base/strings/strcat.h"
#include "base/threading/thread_local.h"
#include "gin/converter.h"
#include "shell/common/api/electron_bindings.h"
#include "shell/common/gin_helper/event_emitter_caller.h"
#include "shell/common/node_bindings.h"
#include "shell/common/node_includes.h"
#include "shell/common/node_util.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h" // nogncheck
namespace electron {
@@ -23,6 +24,23 @@ namespace {
static base::NoDestructor<base::ThreadLocalOwnedPointer<WebWorkerObserver>>
lazy_tls;
// Returns true if `context` belongs to a worklet that runs on a thread
// pooled by Blink's WorkletThreadHolder, where the worker thread can be
// reused for multiple worklet contexts. For these scopes the
// WebWorkerObserver and its NodeBindings must outlive the v8::Context so
// the next pooled context can reuse them — Node.js cannot be re-initialized
// on the same thread (the allocator shim only loads once). See callers of
// blink::WorkletThreadHolder in third_party/blink for the authoritative
// list.
bool IsPooledWorkletContext(v8::Local<v8::Context> context) {
auto* ec = blink::ExecutionContext::From(context);
if (!ec)
return false;
return ec->IsAudioWorkletGlobalScope() || ec->IsPaintWorkletGlobalScope() ||
ec->IsAnimationWorkletGlobalScope() ||
ec->IsSharedStorageWorkletGlobalScope();
}
} // namespace
// static
@@ -48,6 +66,21 @@ WebWorkerObserver::~WebWorkerObserver() = default;
void WebWorkerObserver::WorkerScriptReadyForEvaluation(
v8::Local<v8::Context> worker_context) {
active_context_count_++;
if (environments_.empty()) {
// First context on this thread - do full Node.js initialization.
InitializeNewEnvironment(worker_context);
} else {
// Thread is being reused (AudioWorklet thread pooling). Share the
// existing Node.js environment with the new context instead of
// reinitializing, which would break existing contexts on this thread.
ShareEnvironmentWithContext(worker_context);
}
}
void WebWorkerObserver::InitializeNewEnvironment(
v8::Local<v8::Context> worker_context) {
v8::Context::Scope context_scope(worker_context);
v8::Isolate* const isolate = v8::Isolate::GetCurrent();
v8::MicrotasksScope microtasks_scope(
@@ -106,26 +139,191 @@ void WebWorkerObserver::WorkerScriptReadyForEvaluation(
environments_.insert(std::move(env));
}
void WebWorkerObserver::ShareEnvironmentWithContext(
v8::Local<v8::Context> worker_context) {
v8::Context::Scope context_scope(worker_context);
v8::Isolate* const isolate = v8::Isolate::GetCurrent();
v8::MicrotasksScope microtasks_scope(
worker_context, v8::MicrotasksScope::kDoNotRunMicrotasks);
// Get the existing environment from the first context on this thread.
DCHECK(!environments_.empty());
node::Environment* env = environments_.begin()->get();
// Initialize the V8 context for Node.js use.
v8::Maybe<bool> initialized = node::InitializeContext(worker_context);
CHECK(!initialized.IsNothing() && initialized.FromJust());
// Assign the existing Node.js environment to this new context so that
// node::Environment::GetCurrent(context) returns the shared environment.
env->AssignToContext(worker_context, env->principal_realm(),
node::ContextInfo("electron_worker"));
// Get process and require from the original context to make Node.js
// APIs available in the new context.
v8::Local<v8::Context> original_context = env->context();
v8::Local<v8::Object> original_global = original_context->Global();
v8::Local<v8::Object> new_global = worker_context->Global();
v8::Local<v8::Value> process_value;
CHECK(original_global
->Get(original_context, gin::StringToV8(isolate, "process"))
.ToLocal(&process_value));
v8::Local<v8::Value> require_value;
CHECK(original_global
->Get(original_context, gin::StringToV8(isolate, "require"))
.ToLocal(&require_value));
// Set up 'global' as an alias for globalThis. Node.js bootstrapping normally
// does this during LoadEnvironment, but we skip full bootstrap for shared
// contexts.
new_global
->Set(worker_context, gin::StringToV8(isolate, "global"), new_global)
.Check();
new_global
->Set(worker_context, gin::StringToV8(isolate, "process"), process_value)
.Check();
new_global
->Set(worker_context, gin::StringToV8(isolate, "require"), require_value)
.Check();
// Copy Buffer from the original context if it exists.
v8::Local<v8::Value> buffer_value;
if (original_global->Get(original_context, gin::StringToV8(isolate, "Buffer"))
.ToLocal(&buffer_value) &&
!buffer_value->IsUndefined()) {
new_global
->Set(worker_context, gin::StringToV8(isolate, "Buffer"), buffer_value)
.Check();
}
// Restore the Blink implementations of web APIs that Node.js may
// have deleted. For first-context init this is done by the node_init script
// but we can't run that for shared contexts (it calls internalBinding).
// Instead, copy the blink-prefixed values set during first init.
for (const std::string_view key :
{"fetch", "Response", "FormData", "Request", "Headers", "EventSource"}) {
// First, check if the new context has a working Blink version.
v8::MaybeLocal<v8::Value> blink_value =
new_global->Get(worker_context, gin::StringToV8(isolate, key));
if (!blink_value.IsEmpty() && !blink_value.ToLocalChecked()->IsUndefined())
continue;
// If not, copy from the original context.
std::string blink_key = base::StrCat({"blink", key});
v8::Local<v8::Value> orig_value;
if (original_global->Get(original_context, gin::StringToV8(isolate, key))
.ToLocal(&orig_value) &&
!orig_value->IsUndefined()) {
new_global->Set(worker_context, gin::StringToV8(isolate, key), orig_value)
.Check();
}
}
}
void WebWorkerObserver::ContextWillDestroy(v8::Local<v8::Context> context) {
node::Environment* env = node::Environment::GetCurrent(context);
if (env) {
v8::Context::Scope context_scope(env->context());
gin_helper::EmitEvent(env->isolate(), env->process_object(), "exit");
if (!env)
return;
const bool is_pooled_worklet = IsPooledWorkletContext(context);
active_context_count_--;
if (active_context_count_ == 0) {
// Last context on this thread — full cleanup.
{
v8::Context::Scope context_scope(env->context());
// Emit the "exit" event on the process object. We avoid using
// gin_helper::EmitEvent here because it goes through
// CallMethodWithArgs, which creates a node::CallbackScope. During
// worker shutdown (PrepareForShutdownOnWorkerThread), the
// CallbackScope destructor's InternalCallbackScope::Close() tries to
// process ticks and microtask checkpoints, which can SEGV because the
// worker context is being torn down by Blink.
v8::Isolate* isolate = env->isolate();
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Context> ctx = env->context();
v8::Local<v8::Value> emit_v;
if (env->process_object()
->Get(ctx, gin::StringToV8(isolate, "emit"))
.ToLocal(&emit_v) &&
emit_v->IsFunction()) {
v8::Local<v8::Value> args[] = {gin::StringToV8(isolate, "exit")};
v8::TryCatch try_catch(isolate);
emit_v.As<v8::Function>()
->Call(ctx, env->process_object(), 1, args)
.FromMaybe(v8::Local<v8::Value>());
// We are mid-teardown and about to destroy the worker's
// node::Environment, so we cannot let an exception thrown by an
// 'exit' listener propagate back into Blink (it would assert in
// V8::FromJustIsNothing on the next call into V8). Log it and
// explicitly reset the TryCatch so the destructor doesn't rethrow.
if (try_catch.HasCaught()) {
if (auto message = try_catch.Message(); !message.IsEmpty()) {
std::string str;
if (gin::ConvertFromV8(isolate, message->Get(), &str))
LOG(ERROR) << "Exception thrown from worker 'exit' handler: "
<< str;
}
try_catch.Reset();
}
}
}
// Prevent UvRunOnce from using the environment after it's destroyed.
node_bindings_->set_uv_env(nullptr);
// Stop the embed thread before destroying environments. The embed
// thread's PollEvents and FreeEnvironment's uv_run both compete for
// completions on the same libuv event loop; on Windows (IOCP) this
// race can deadlock. Joining the embed thread first eliminates the
// contention so FreeEnvironment's uv_run can drain handles cleanly.
// For pooled worklets the thread is restarted in
// InitializeNewEnvironment via PrepareEmbedThread + StartPolling.
node_bindings_->StopPolling();
// Destroying the node environment will also run the uv loop.
{
util::ExplicitMicrotasksScope microtasks_scope(
context->GetMicrotaskQueue());
environments_.clear();
}
// ElectronBindings is tracking node environments.
electron_bindings_->EnvironmentDestroyed(env);
// For non-pooled worker contexts (e.g., dedicated workers) Blink does
// not reuse the worker thread, so tear down the observer completely.
//
// For pooled worklet contexts (AudioWorklet, PaintWorklet,
// AnimationWorklet, SharedStorageWorklet — see
// blink::WorkletThreadHolder) the same NodeBindings must be reused
// for the next context on the thread because Node.js cannot be
// re-initialized on the same thread. Keep the observer alive and let
// the next WorkerScriptReadyForEvaluation call
// InitializeNewEnvironment, which restarts the embed thread via
// PrepareEmbedThread + StartPolling.
if (!is_pooled_worklet) {
lazy_tls->Set(nullptr); // destroys *this; do not access members below
return;
}
} else {
// Other contexts still use the shared environment. Just unassign
// this context from the environment if it's not the primary context
// (the primary context must stay assigned because env->context()
// references it, and UvRunOnce enters that context scope).
if (context != env->context()) {
env->UnassignFromContext(context);
}
// If the destroyed context IS the primary context, we leave the env
// assigned to it. The env's PrincipalRealm holds a Global<Context>
// reference that keeps the V8 context alive even though Blink has
// torn down its side. This is safe because UvRunOnce only needs
// the V8 context scope, not Blink-side objects.
}
// Destroying the node environment will also run the uv loop.
{
util::ExplicitMicrotasksScope microtasks_scope(
context->GetMicrotaskQueue());
base::EraseIf(environments_,
[env](auto const& item) { return item.get() == env; });
}
// ElectronBindings is tracking node environments.
electron_bindings_->EnvironmentDestroyed(env);
if (lazy_tls->Get())
lazy_tls->Set(nullptr);
}
} // namespace electron

View File

@@ -40,9 +40,17 @@ class WebWorkerObserver {
void ContextWillDestroy(v8::Local<v8::Context> context);
private:
// Full initialization for the first context on a thread.
void InitializeNewEnvironment(v8::Local<v8::Context> context);
// Share existing environment with a new context on a reused thread.
void ShareEnvironmentWithContext(v8::Local<v8::Context> context);
std::unique_ptr<NodeBindings> node_bindings_;
std::unique_ptr<ElectronBindings> electron_bindings_;
base::flat_set<std::shared_ptr<node::Environment>> environments_;
// Number of active contexts using the environment on this thread.
size_t active_context_count_ = 0;
};
} // namespace electron

View File

@@ -1625,6 +1625,27 @@ describe('chromium features', () => {
expect(data).to.equal('function function function function function');
});
it('AudioWorklet keeps node integration across pooled worker threads', async () => {
// Regression test for https://github.com/electron/electron/issues/41263.
// Blink pools the AudioWorklet backing thread (Chromium CL:5270028) so
// the Nth+ AudioWorklet on a page reuses the same thread; the page
// creates several AudioWorklet contexts in sequence and asserts node
// integration is wired up in every one of them.
const w = new BrowserWindow({
show: false,
webPreferences: {
nodeIntegration: true,
nodeIntegrationInWorker: true,
contextIsolation: false
}
});
w.loadURL(`file://${fixturesPath}/pages/audio-worklet.html`);
const [, results] = await once(ipcMain, 'audio-worklet-result');
expect(results).to.be.an('array').with.length.greaterThan(0);
for (const r of results) expect(r).to.equal('ok');
});
describe('SharedWorker', () => {
it('can work', async () => {
const w = new BrowserWindow({ show: false });

44
spec/fixtures/pages/audio-worklet.html vendored Normal file
View File

@@ -0,0 +1,44 @@
<html>
<body>
<script type="text/javascript" charset="utf-8">
const { ipcRenderer } = require('electron');
// Create a number of AudioContext + AudioWorklet pairs in sequence so
// that Blink's WorkletThreadHolder pools and reuses the underlying
// worker thread (Chromium CL:5270028). For each context we ask the
// worklet to report whether `require` is a function and post that back
// via its MessagePort. The bug being guarded is that the Nth+ pooled
// worklet would silently lose its Node.js environment, so the test
// must run enough iterations to exercise thread reuse.
const NUM_CONTEXTS = 6;
async function runOne(index) {
const audioCtx = new AudioContext();
try {
await audioCtx.audioWorklet.addModule('../workers/audio_worklet_node.js');
const node = new AudioWorkletNode(audioCtx, 'node-integration-probe');
const reply = new Promise((resolve) => {
node.port.onmessage = (e) => resolve(e.data);
});
node.port.postMessage('probe');
node.connect(audioCtx.destination);
return await reply;
} finally {
await audioCtx.close();
}
}
(async () => {
const results = [];
for (let i = 0; i < NUM_CONTEXTS; i++) {
try {
results.push(await runOne(i));
} catch (err) {
results.push(`error: ${err && err.message ? err.message : err}`);
}
}
ipcRenderer.send('audio-worklet-result', results);
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,28 @@
// Reports whether the Node.js environment is wired up inside this
// AudioWorklet's global scope. Used by spec/fixtures/pages/audio-worklet.html
// to verify that nodeIntegrationInWorker keeps working when Blink reuses a
// pooled worker thread for multiple AudioWorklet contexts.
class NodeIntegrationProbeProcessor extends AudioWorkletProcessor {
constructor () {
super();
this.port.onmessage = () => {
let info;
try {
// require should be a function and `node:timers` should resolve.
const ok = typeof require === 'function' &&
typeof require('node:timers').setImmediate === 'function' &&
typeof process === 'object';
info = ok ? 'ok' : 'missing';
} catch (err) {
info = `throw: ${err && err.message ? err.message : err}`;
}
this.port.postMessage(info);
};
}
process () {
return true;
}
}
registerProcessor('node-integration-probe', NodeIntegrationProbeProcessor);