mirror of
https://github.com/electron/electron.git
synced 2026-05-02 03:00:22 -04:00
* fix: nodeIntegrationInWorker not working in AudioWorklet Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com> * 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. Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com> * chore: address review feedback Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com> * 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. Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com> * chore: address outstanding feedback Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com> * chore: update patches --------- Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com> Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com> Co-authored-by: PatchUp <73610968+patchup[bot]@users.noreply.github.com>
288 lines
11 KiB
C++
288 lines
11 KiB
C++
// Copyright (c) 2013 GitHub, Inc.
|
|
// Use of this source code is governed by the MIT license that can be
|
|
// found in the LICENSE file.
|
|
|
|
#include "shell/renderer/electron_renderer_client.h"
|
|
|
|
#include <algorithm>
|
|
|
|
#include "content/public/renderer/render_frame.h"
|
|
#include "electron/fuses.h"
|
|
#include "net/http/http_request_headers.h"
|
|
#include "shell/common/api/electron_bindings.h"
|
|
#include "shell/common/gin_helper/dictionary.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 "shell/common/v8_util.h"
|
|
#include "shell/renderer/electron_render_frame_observer.h"
|
|
#include "shell/renderer/web_worker_observer.h"
|
|
#include "third_party/blink/public/common/web_preferences/web_preferences.h"
|
|
#include "third_party/blink/public/web/web_document.h"
|
|
#include "third_party/blink/public/web/web_local_frame.h"
|
|
#include "third_party/blink/renderer/core/execution_context/execution_context.h" // nogncheck
|
|
#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 {
|
|
|
|
ElectronRendererClient::ElectronRendererClient()
|
|
: node_bindings_{NodeBindings::Create(
|
|
NodeBindings::BrowserEnvironment::kRenderer)},
|
|
electron_bindings_{
|
|
std::make_unique<ElectronBindings>(node_bindings_->uv_loop())} {}
|
|
|
|
ElectronRendererClient::~ElectronRendererClient() = default;
|
|
|
|
void ElectronRendererClient::PostIOThreadCreated(
|
|
base::SingleThreadTaskRunner* io_thread_task_runner) {
|
|
// Freezing flags after init conflicts with node in the renderer.
|
|
// We do this here in order to avoid having to patch the ctor in
|
|
// content/renderer/render_process_impl.cc.
|
|
v8::V8::SetFlagsFromString("--no-freeze-flags-after-init");
|
|
}
|
|
|
|
void ElectronRendererClient::RenderFrameCreated(
|
|
content::RenderFrame* render_frame) {
|
|
new ElectronRenderFrameObserver(render_frame, this);
|
|
RendererClientBase::RenderFrameCreated(render_frame);
|
|
}
|
|
|
|
void ElectronRendererClient::RunScriptsAtDocumentStart(
|
|
content::RenderFrame* render_frame) {
|
|
RendererClientBase::RunScriptsAtDocumentStart(render_frame);
|
|
// Inform the document start phase.
|
|
v8::HandleScope handle_scope(v8::Isolate::GetCurrent());
|
|
node::Environment* env = GetEnvironment(render_frame);
|
|
if (env) {
|
|
v8::Context::Scope context_scope(env->context());
|
|
gin_helper::EmitEvent(env->isolate(), env->process_object(),
|
|
"document-start");
|
|
}
|
|
}
|
|
|
|
void ElectronRendererClient::RunScriptsAtDocumentEnd(
|
|
content::RenderFrame* render_frame) {
|
|
RendererClientBase::RunScriptsAtDocumentEnd(render_frame);
|
|
// Inform the document end phase.
|
|
v8::HandleScope handle_scope(v8::Isolate::GetCurrent());
|
|
node::Environment* env = GetEnvironment(render_frame);
|
|
if (env) {
|
|
v8::Context::Scope context_scope(env->context());
|
|
gin_helper::EmitEvent(env->isolate(), env->process_object(),
|
|
"document-end");
|
|
}
|
|
}
|
|
|
|
void ElectronRendererClient::UndeferLoad(content::RenderFrame* render_frame) {
|
|
render_frame->GetWebFrame()->GetDocumentLoader()->SetDefersLoading(
|
|
blink::LoaderFreezeMode::kNone);
|
|
}
|
|
|
|
void ElectronRendererClient::DidCreateScriptContext(
|
|
v8::Isolate* const isolate,
|
|
v8::Local<v8::Context> renderer_context,
|
|
content::RenderFrame* render_frame) {
|
|
// TODO(zcbenz): Do not create Node environment if node integration is not
|
|
// enabled.
|
|
|
|
// Only load Node.js if we are a main frame or a devtools extension
|
|
// unless Node.js support has been explicitly enabled for subframes.
|
|
if (!ShouldLoadPreload(isolate, renderer_context, render_frame))
|
|
return;
|
|
|
|
injected_frames_.insert(render_frame);
|
|
|
|
if (!node_integration_initialized_) {
|
|
node_integration_initialized_ = true;
|
|
node_bindings_->Initialize(isolate, renderer_context);
|
|
node_bindings_->PrepareEmbedThread();
|
|
}
|
|
|
|
// Setup node tracing controller.
|
|
if (!node::tracing::TraceEventHelper::GetAgent()) {
|
|
auto* tracing_agent = new node::tracing::Agent();
|
|
node::tracing::TraceEventHelper::SetAgent(tracing_agent);
|
|
}
|
|
|
|
// Setup node environment for each window.
|
|
v8::Maybe<bool> initialized = node::InitializeContext(renderer_context);
|
|
CHECK(!initialized.IsNothing() && initialized.FromJust());
|
|
|
|
// Before we load the node environment, let's tell blink to hold off on
|
|
// loading the body of this frame. We will undefer the load once the preload
|
|
// script has finished. This allows our preload script to run async (E.g.
|
|
// with ESM) without the preload being in a race
|
|
render_frame->GetWebFrame()->GetDocumentLoader()->SetDefersLoading(
|
|
blink::LoaderFreezeMode::kStrict);
|
|
|
|
std::shared_ptr<node::Environment> env = node_bindings_->CreateEnvironment(
|
|
isolate, renderer_context, nullptr, 0,
|
|
base::BindRepeating(&ElectronRendererClient::UndeferLoad,
|
|
base::Unretained(this), render_frame));
|
|
|
|
// We need to use the Blink implementation of fetch in the renderer process
|
|
// Node.js deletes the global fetch function when their fetch implementation
|
|
// is disabled, so we need to save and re-add it after the Node.js environment
|
|
// is loaded. See corresponding change in node/init.ts.
|
|
v8::Local<v8::Object> global = renderer_context->Global();
|
|
|
|
std::vector<std::string> keys = {"fetch", "Response", "FormData",
|
|
"Request", "Headers", "EventSource"};
|
|
for (const auto& key : keys) {
|
|
v8::MaybeLocal<v8::Value> value =
|
|
global->Get(renderer_context, gin::StringToV8(isolate, key));
|
|
if (!value.IsEmpty()) {
|
|
std::string blink_key = "blink" + key;
|
|
global
|
|
->Set(renderer_context, gin::StringToV8(isolate, blink_key),
|
|
value.ToLocalChecked())
|
|
.Check();
|
|
}
|
|
}
|
|
|
|
// If we have disabled the site instance overrides we should prevent loading
|
|
// any non-context aware native module.
|
|
env->options()->force_context_aware = true;
|
|
|
|
// We do not want to crash the renderer process on unhandled rejections.
|
|
env->options()->unhandled_rejections = "warn-with-error-code";
|
|
|
|
environments_.insert(env);
|
|
|
|
// Add Electron extended APIs.
|
|
electron_bindings_->BindTo(env->isolate(), env->process_object());
|
|
gin_helper::Dictionary process_dict(env->isolate(), env->process_object());
|
|
BindProcess(env->isolate(), &process_dict, render_frame);
|
|
|
|
// Load everything.
|
|
node_bindings_->LoadEnvironment(env.get());
|
|
|
|
if (node_bindings_->uv_env() == nullptr) {
|
|
// Make uv loop being wrapped by window context.
|
|
node_bindings_->set_uv_env(env.get());
|
|
|
|
// Give the node loop a run to make sure everything is ready.
|
|
node_bindings_->StartPolling();
|
|
}
|
|
}
|
|
|
|
void ElectronRendererClient::WillReleaseScriptContext(
|
|
v8::Isolate* const isolate,
|
|
v8::Local<v8::Context> context,
|
|
content::RenderFrame* render_frame) {
|
|
if (injected_frames_.erase(render_frame) == 0)
|
|
return;
|
|
|
|
node::Environment* env = node::Environment::GetCurrent(context);
|
|
const auto iter = std::ranges::find_if(
|
|
environments_, [env](auto& item) { return env == item.get(); });
|
|
if (iter == environments_.end())
|
|
return;
|
|
|
|
gin_helper::EmitEvent(isolate, env->process_object(), "exit");
|
|
|
|
// The main frame may be replaced.
|
|
if (env == node_bindings_->uv_env())
|
|
node_bindings_->set_uv_env(nullptr);
|
|
|
|
// Destroying the node environment will also run the uv loop.
|
|
{
|
|
util::ExplicitMicrotasksScope microtasks_scope(
|
|
context->GetMicrotaskQueue());
|
|
environments_.erase(iter);
|
|
}
|
|
|
|
// ElectronBindings is tracking node environments.
|
|
electron_bindings_->EnvironmentDestroyed(env);
|
|
}
|
|
|
|
namespace {
|
|
|
|
bool WorkerHasNodeIntegration(blink::ExecutionContext* ec) {
|
|
// We do not create a Node.js environment in service or shared workers
|
|
// 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 — 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;
|
|
|
|
// Read the nodeIntegrationInWorker preference from the worker's settings,
|
|
// which were copied from the initiating frame's WebPreferences at worker
|
|
// creation time. This ensures that in-process child windows with different
|
|
// webPreferences get the correct per-frame value rather than a process-wide
|
|
// value.
|
|
auto* worker_settings = wgs->GetWorkerSettings();
|
|
return worker_settings && worker_settings->NodeIntegrationInWorker();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void ElectronRendererClient::WorkerScriptReadyForEvaluationOnWorkerThread(
|
|
v8::Local<v8::Context> context) {
|
|
auto* ec = blink::ExecutionContext::From(context);
|
|
if (!WorkerHasNodeIntegration(ec))
|
|
return;
|
|
|
|
auto* current = WebWorkerObserver::GetCurrent();
|
|
if (!current)
|
|
current = WebWorkerObserver::Create();
|
|
current->WorkerScriptReadyForEvaluation(context);
|
|
}
|
|
|
|
void ElectronRendererClient::WillDestroyWorkerContextOnWorkerThread(
|
|
v8::Local<v8::Context> context) {
|
|
auto* ec = blink::ExecutionContext::From(context);
|
|
if (!WorkerHasNodeIntegration(ec))
|
|
return;
|
|
|
|
auto* current = WebWorkerObserver::GetCurrent();
|
|
if (current)
|
|
current->ContextWillDestroy(context);
|
|
}
|
|
|
|
void ElectronRendererClient::SetUpWebAssemblyTrapHandler() {
|
|
#if ((BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC)) && \
|
|
defined(ARCH_CPU_X86_64)) || \
|
|
((BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_MAC)) && defined(ARCH_CPU_ARM64))
|
|
if (electron::fuses::IsWasmTrapHandlersEnabled()) {
|
|
electron::SetUpWebAssemblyTrapHandler();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
node::Environment* ElectronRendererClient::GetEnvironment(
|
|
content::RenderFrame* render_frame) const {
|
|
if (!injected_frames_.contains(render_frame))
|
|
return nullptr;
|
|
v8::HandleScope handle_scope(v8::Isolate::GetCurrent());
|
|
auto context =
|
|
GetContext(render_frame->GetWebFrame(), v8::Isolate::GetCurrent());
|
|
node::Environment* env = node::Environment::GetCurrent(context);
|
|
|
|
return std::ranges::contains(environments_, env,
|
|
[](auto const& item) { return item.get(); })
|
|
? env
|
|
: nullptr;
|
|
}
|
|
|
|
} // namespace electron
|