Compare commits

..

1 Commits

Author SHA1 Message Date
David Sanders
aa00a51914 build: don't use //third_party/depot_tools in gn build scripts 2026-04-09 00:55:34 -07:00
14 changed files with 122 additions and 538 deletions

View File

@@ -34,48 +34,6 @@ 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
@@ -113,56 +71,3 @@ 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

@@ -11,10 +11,9 @@ const path = require('node:path');
const args = minimist(process.argv.slice(2), { string: ['outDir'] });
const { getOutDir } = require('./lib/utils');
const { getDepotToolsEnv, getOutDir } = require('./lib/utils');
const SOURCE_ROOT = path.normalize(path.dirname(__dirname));
const DEPOT_TOOLS = path.resolve(SOURCE_ROOT, '..', 'third_party', 'depot_tools');
const OUT_DIR = getOutDir({ outDir: args.outDir });
if (!OUT_DIR) {
@@ -22,12 +21,10 @@ if (!OUT_DIR) {
}
const env = {
...getDepotToolsEnv(),
CHROMIUM_BUILDTOOLS_PATH: path.resolve(SOURCE_ROOT, '..', 'buildtools'),
DEPOT_TOOLS_WIN_TOOLCHAIN: '0',
...process.env
};
// Users may not have depot_tools in PATH.
env.PATH = `${env.PATH}${path.delimiter}${DEPOT_TOOLS}`;
const gnCheckDirs = [
'//electron:electron_lib',

View File

@@ -205,11 +205,37 @@ def get_buildtools_executable(name):
path += '.exe'
return path
def get_depot_tools_executable(name):
buildtools = os.path.realpath(
os.path.join(ELECTRON_DIR, '..', 'third_party', 'depot_tools'))
def get_depot_tools_env():
def find_depot_tools_on_path():
cmd = 'where' if sys.platform == 'win32' else 'which'
try:
subprocess.check_call([cmd, 'gclient'])
return os.environ.copy()
except subprocess.CalledProcessError:
return None
path = os.path.join(buildtools, name)
if sys.platform == 'win32':
path += '.bat'
return path
def check_for_build_tools():
try:
output = subprocess.check_output(
'electron-build-tools show env --json',
shell=True, stderr=subprocess.DEVNULL
)
env = os.environ.copy()
env.update(json.loads(output.decode().strip()))
return env
except subprocess.CalledProcessError:
return None
depot_tools_env = check_for_build_tools()
if depot_tools_env is None:
depot_tools_env = find_depot_tools_on_path()
if depot_tools_env is None:
raise RuntimeError("Couldn't find depot_tools, ensure it's on your PATH")
if 'CHROMIUM_BUILDTOOLS_PATH' not in depot_tools_env:
raise RuntimeError(
'CHROMIUM_BUILDTOOLS_PATH environment variable must be set'
)
return depot_tools_env

View File

@@ -168,12 +168,60 @@ function getChromiumVersionFromDEPS (depsContent) {
return CHROMIUM_VERSION_DEPS_REGEX.exec(depsContent)?.[1] ?? null;
}
function getDepotToolsEnv () {
let depotToolsEnv;
const findDepotToolsOnPath = () => {
const result = childProcess.spawnSync(
os.platform() === 'win32' ? 'where' : 'which',
['gclient']
);
if (result.status === 0) {
return process.env;
}
};
const checkForBuildTools = () => {
const result = childProcess.spawnSync(
'electron-build-tools',
['show', 'env', '--json'],
{ shell: true }
);
if (result.status === 0) {
return {
...process.env,
...JSON.parse(result.stdout.toString().trim())
};
}
};
try {
depotToolsEnv = checkForBuildTools();
if (!depotToolsEnv) depotToolsEnv = findDepotToolsOnPath();
} catch {}
if (!depotToolsEnv) {
throw new Error("Couldn't find depot_tools, ensure it's on your PATH");
}
if (!('CHROMIUM_BUILDTOOLS_PATH' in depotToolsEnv)) {
throw new Error(
'CHROMIUM_BUILDTOOLS_PATH environment variable must be set'
);
}
return depotToolsEnv;
}
module.exports = {
chunkFilenames,
compareVersions,
findMatchingFiles,
getChromiumVersionFromDEPS,
getCurrentBranch,
getDepotToolsEnv,
getElectronExec,
getOutDir,
getAbsoluteElectronExec,

View File

@@ -6,10 +6,9 @@ import { streamArray as streamJsonStreamArray } from 'stream-json/streamers/Stre
import * as childProcess from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { chunkFilenames, findMatchingFiles } from './lib/utils';
import { chunkFilenames, findMatchingFiles, getDepotToolsEnv } from './lib/utils';
const SOURCE_ROOT = path.normalize(path.dirname(__dirname));
const LLVM_BIN = path.resolve(
@@ -20,7 +19,6 @@ const LLVM_BIN = path.resolve(
'Release+Asserts',
'bin'
);
const PLATFORM = os.platform();
type SpawnAsyncResult = {
stdout: string;
@@ -63,53 +61,6 @@ async function spawnAsync (
});
}
function getDepotToolsEnv (): NodeJS.ProcessEnv {
let depotToolsEnv;
const findDepotToolsOnPath = () => {
const result = childProcess.spawnSync(
PLATFORM === 'win32' ? 'where' : 'which',
['gclient']
);
if (result.status === 0) {
return process.env;
}
};
const checkForBuildTools = () => {
const result = childProcess.spawnSync(
'electron-build-tools',
['show', 'env', '--json'],
{ shell: true }
);
if (result.status === 0) {
return {
...process.env,
...JSON.parse(result.stdout.toString().trim())
};
}
};
try {
depotToolsEnv = findDepotToolsOnPath();
if (!depotToolsEnv) depotToolsEnv = checkForBuildTools();
} catch {}
if (!depotToolsEnv) {
throw new Error("Couldn't find depot_tools, ensure it's on your PATH");
}
if (!('CHROMIUM_BUILDTOOLS_PATH' in depotToolsEnv)) {
throw new Error(
'CHROMIUM_BUILDTOOLS_PATH environment variable must be set'
);
}
return depotToolsEnv;
}
async function runClangTidy (
outDir: string,
filenames: string[],

View File

@@ -2,23 +2,22 @@ import os
import subprocess
import sys
from lib.util import get_depot_tools_executable
from lib.util import get_depot_tools_env
SOURCE_ROOT = os.path.dirname(os.path.dirname(__file__))
# Helper to run gn format on multiple files
# (gn only formats a single file at a time)
def main():
new_env = os.environ.copy()
new_env = get_depot_tools_env()
new_env['DEPOT_TOOLS_WIN_TOOLCHAIN'] = '0'
new_env['CHROMIUM_BUILDTOOLS_PATH'] = os.path.realpath(
os.path.join(SOURCE_ROOT, '..', 'buildtools')
)
gn_path = get_depot_tools_executable('gn')
for gn_file in sys.argv[1:]:
subprocess.check_call(
[gn_path, 'format', gn_file],
['gn', 'format', gn_file],
env=new_env
)

View File

@@ -529,7 +529,14 @@ NodeBindings::NodeBindings(BrowserEnvironment browser_env)
uv_loop_{InitEventLoop(browser_env, &worker_loop_)} {}
NodeBindings::~NodeBindings() {
StopPolling();
// Quit the embed thread.
embed_closed_ = true;
uv_sem_post(&embed_sem_);
WakeupEmbedThread();
// Wait for everything to be done.
uv_thread_join(&embed_thread_);
// Clear uv.
uv_sem_destroy(&embed_sem_);
@@ -540,26 +547,6 @@ 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() <=
@@ -946,21 +933,12 @@ void NodeBindings::PrepareEmbedThread() {
if (initialized_)
return;
// 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;
}
// 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);
uv_thread_create(&embed_thread_, EmbedThreadRunner, this);
}

View File

@@ -157,12 +157,6 @@ 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.
@@ -231,11 +225,6 @@ 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,7 +25,6 @@
#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 {
@@ -207,20 +206,11 @@ 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 — those share the page's V8 context where
// Node is already wired up.
// created on the main thread.
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;
@@ -243,9 +233,9 @@ void ElectronRendererClient::WorkerScriptReadyForEvaluationOnWorkerThread(
return;
auto* current = WebWorkerObserver::GetCurrent();
if (!current)
current = WebWorkerObserver::Create();
current->WorkerScriptReadyForEvaluation(context);
if (current)
return;
WebWorkerObserver::Create()->WorkerScriptReadyForEvaluation(context);
}
void ElectronRendererClient::WillDestroyWorkerContextOnWorkerThread(

View File

@@ -10,12 +10,11 @@
#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 {
@@ -24,23 +23,6 @@ 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
@@ -66,21 +48,6 @@ 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(
@@ -139,191 +106,26 @@ void WebWorkerObserver::InitializeNewEnvironment(
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)
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.
if (env) {
v8::Context::Scope context_scope(env->context());
gin_helper::EmitEvent(env->isolate(), env->process_object(), "exit");
}
// 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,17 +40,9 @@ 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,27 +1625,6 @@ 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 });

View File

@@ -1,44 +0,0 @@
<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

@@ -1,28 +0,0 @@
// 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);