mirror of
https://github.com/electron/electron.git
synced 2026-05-02 03:00:22 -04:00
* feat: capture JS stack trace on renderer OOM
When a renderer process approaches its V8 heap limit, capture the
JavaScript stack trace and write it to both a Crashpad crash key
("js-oom-stack") and stderr.
The stack trace is captured via RequestInterrupt rather than directly
inside the NearHeapLimitCallback because CurrentStackTrace is unsafe
to call during OOM — V8 FATALs on optimized (TurboFan) frames that
have had their deoptimization data garbage-collected. RequestInterrupt
defers the capture to the next V8 safe point, where all frames are
guaranteed to have deopt data available. This matches Node.js's
approach of never capturing JS stacks inside the heap limit callback.
The callback is registered once per isolate via an atomic guard in
RendererClientBase::DidCreateScriptContext, preventing the CHECK
failure V8 raises on duplicate AddNearHeapLimitCallback registrations
(which would otherwise occur on page navigations or multiple contexts).
Refs: #46078
Made-with: Cursor
Co-authored-by: Alexey Kozy <alexey@anysphere.co>
* Update shell/renderer/oom_stack_trace.cc
Co-authored-by: Niklas Wenzel <dev@nikwen.de>
Co-authored-by: Alexey Kozy <alexey@anysphere.co>
* Update shell/renderer/oom_stack_trace.cc
Co-authored-by: Niklas Wenzel <dev@nikwen.de>
Co-authored-by: Alexey Kozy <alexey@anysphere.co>
* test: add crash reporter test for OOM JS stack trace
Add a test that verifies the `electron.v8-oom.stack` crash key contains
the JS stack trace (including function names) when a renderer process
runs out of memory. Also deduplicate the heap info formatting in
oom_stack_trace.cc.
Refs: #46078
Made-with: Cursor
Co-authored-by: Alexey Kozy <alexey@anysphere.co>
* fix: lint formatting in oom_stack_trace.cc
Made-with: Cursor
Co-authored-by: Alexey Kozy <alexey@anysphere.co>
* fix: use proper logger API instead of cstdio
Co-authored-by: Alexey Kozy <alexey@anysphere.co>
* fix: check heap headroom before capturing OOM stack trace
deepak1556: "Should there be check for available heap size [for]
CurrentStackTrace and formatting"
CurrentStackTrace allocates StackTraceInfo + StackFrameInfo on the V8
heap. If the 20 MB bump is partially consumed by the time the interrupt
fires, these allocations trigger a secondary OOM. Guard with a 2 MB
headroom check.
Made-with: Cursor
Co-authored-by: Alexey Kozy <alexey@anysphere.co>
* fix: handle V8 cage limit when bumping heap for OOM stack capture
deepak1556: "Does this bumping work when we are at the cage limit of
4GB"
V8's pointer compression cage caps the heap at ~4 GB. When
current_heap_limit is already near the ceiling, our 20 MB bump gets
clamped to zero and the interrupt never fires. Detect this and record
heap info as the final crash key instead of waiting for a stack trace
that won't arrive.
Made-with: Cursor
Co-authored-by: Alexey Kozy <alexey@anysphere.co>
* feat: add V8 heap statistics as OOM crash keys
deepak1556: "V8 seems to capture heap stats as crash keys but it gets
missed today due to the OOM callback override... wonder if we can
include that to get some more heuristics in the dump."
Record heap used/total/limit/available, per-space stats for old_space
and large_object_space, native/detached context counts, and utilization
percentage as crash keys. Also add heap stats in the V8OOMErrorCallback
in node_bindings.cc for the final OOM crash report.
Made-with: Cursor
Co-authored-by: Alexey Kozy <alexey@anysphere.co>
* feat: support worker thread isolates for OOM stack trace
deepak1556: "You need a separate registration for worker threads via
WorkerScriptReadyForEvaluationOnWorkerThread but that also means the
process global g_registered_isolate would break."
Chromium has one V8 isolate per thread (main + one per web worker), so
thread_local is equivalent to per-isolate storage. Replace the global
atomic + mutex/set with a constinit thread_local OomState* that holds
the isolate pointer and per-isolate is_in_oom flag. The void* data
parameter on AddNearHeapLimitCallback delivers OomState* directly into
callbacks, so the hot path needs no TLS lookup.
Add WorkerScriptReadyForEvaluationOnWorkerThread and
WillDestroyWorkerContextOnWorkerThread overrides to RendererClientBase
so both ElectronRendererClient and ElectronSandboxedRendererClient get
worker OOM registration. Update ElectronRendererClient to call the base
class in both worker lifecycle methods.
Add a web worker OOM test that spawns a dedicated Worker with a memory
leak and verifies the stack trace captures the worker function name.
Made-with: Cursor
Co-authored-by: Alexey Kozy <alexey@anysphere.co>
* fix: register OOM callback for all script contexts
When context isolation is enabled, ShouldNotifyClient skips
DidCreateScriptContext for the main world, but user JS still runs there
and can OOM. Register in DidInstallConditionalFeatures which fires for
every script context. The TLS dedup guard prevents double-registration
on the same isolate.
Made-with: Cursor
Co-authored-by: Alexey Kozy <alexey@anysphere.co>
* fix: guard against division by zero and cage size changes in OOM handler
Add a zero-guard on heap_size_limit before computing utilization
percentage — maximizes robustness in an OOM code path.
Add static_assert on kPtrComprCageReservationSize to catch any
upstream V8 change to the cage size at compile time.
Made-with: Cursor
Co-authored-by: Alexey Kozy <alexey@anysphere.co>
* fix: address review feedback on OOM stack trace PR
- Remove redundant RegisterOomStackTraceCallback from
electron_render_frame_observer.cc; DidCreateScriptContext is sufficient
since main world and isolated world share the same isolate
- Replace thread_local OomState* with base::ThreadLocalOwnedPointer
wrapped in base::NoDestructor per Chromium style for non-trivially
destructible types
- Change heap-headroom and cage-limit logs from ERROR to INFO since
users cannot act on these diagnostics
- Add comment explaining why base class is called last in
WillDestroyWorkerContextOnWorkerThread (OOM deregistration ordering)
Made-with: Cursor
Co-authored-by: Alexey Kozy <alexey@anysphere.co>
* fix: skip OOM stack trace registration for worklet contexts
Worklets can share a thread and isolate via WorkletThreadHolder's
per-process singleton pattern. With per-thread OOM state, the first
worklet to be destroyed would prematurely remove the callback for
any remaining worklets on the same thread. Skip worklets entirely
to avoid this; can be revisited with ref-counting if needed.
Made-with: Cursor
Co-authored-by: Alexey Kozy <alexey@anysphere.co>
* fix: prevent dangling raw_ptr<v8::Isolate> in OOM state
The OomState held a raw_ptr<v8::Isolate> that outlived the isolate on
the main thread: gin::IsolateHolder destroyed the isolate during
shutdown, but the OomState (stored in thread-local storage) was only
released later in JavascriptEnvironment::~JavascriptEnvironment. This
triggers a dangling pointer check when building with
enable_dangling_raw_ptr_checks.
Register OomState as a gin::PerIsolateData::DisposeObserver so it
clears the raw_ptr and removes the NearHeapLimitCallback before the
isolate is destroyed, regardless of destructor ordering.
Suggested-by: Deepak Mohan
Made-with: Cursor
Co-authored-by: Alexey Kozy <alexey@anysphere.co>
* test: verify OOM crash keys end-to-end via crash reporter
Replace stderr-based OOM tests with end-to-end crash dump validation.
Instead of parsing log output, start a crash reporter server, trigger
renderer OOM, and verify the uploaded crash dump contains the expected
`electron.v8-oom.*` annotations — the same code path production crash
reports take.
Consolidate all OOM test scenarios (basic heap leak, JSON.stringify,
web worker) into a single `describe('OOM crash keys')` block inside
api-crash-reporter-spec using the existing crash fixture app with new
renderer-oom-json and renderer-oom-worker crash types.
The web worker test verifies that OOM crash keys are present but does
not assert on the JS function name: the 20 MB heap bump may be
exhausted before V8 reaches a safe point to fire the stack-capture
interrupt, leaving the crash key at "(stack pending)". Increasing the
bump or switching to a synchronous capture strategy would fix this but
is left for a follow-up.
Remove the standalone oom-stack-trace-spec.ts and its fixture app.
Made-with: Cursor
Co-authored-by: Alexey Kozy <alexey@anysphere.co>
* chore: make linter happy
---------
Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Alexey Kozy <alexey@anysphere.co>
Co-authored-by: Charles Kerr <charles@charleskerr.com>
1143 lines
41 KiB
C++
1143 lines
41 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/common/node_bindings.h"
|
|
|
|
#include <algorithm>
|
|
#include <iostream>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "base/allocator/partition_allocator/src/partition_alloc/oom.h"
|
|
#include "base/base_paths.h"
|
|
#include "base/command_line.h"
|
|
#include "base/containers/fixed_flat_set.h"
|
|
#include "base/environment.h"
|
|
#include "base/path_service.h"
|
|
#include "base/run_loop.h"
|
|
#include "base/strings/string_number_conversions.h"
|
|
#include "base/strings/string_split.h"
|
|
#include "base/strings/utf_string_conversions.h"
|
|
#include "base/task/single_thread_task_runner.h"
|
|
#include "base/trace_event/trace_event.h"
|
|
#include "chrome/common/chrome_version.h"
|
|
#include "content/public/common/content_paths.h"
|
|
#include "content/public/renderer/render_frame.h"
|
|
#include "electron/buildflags/buildflags.h"
|
|
#include "electron/electron_version.h"
|
|
#include "electron/fuses.h"
|
|
#include "electron/mas.h"
|
|
#include "shell/browser/api/electron_api_app.h"
|
|
#include "shell/common/api/electron_bindings.h"
|
|
#include "shell/common/electron_command_line.h"
|
|
#include "shell/common/gin_converters/callback_converter.h"
|
|
#include "shell/common/gin_converters/file_path_converter.h"
|
|
#include "shell/common/gin_helper/dictionary.h"
|
|
#include "shell/common/gin_helper/event.h"
|
|
#include "shell/common/gin_helper/event_emitter_caller.h"
|
|
#include "shell/common/mac/main_application_bundle.h"
|
|
#include "shell/common/node_includes.h"
|
|
#include "shell/common/node_util.h"
|
|
#include "shell/common/options_switches.h"
|
|
#include "shell/common/platform_util.h"
|
|
#include "shell/common/process_util.h"
|
|
#include "shell/common/world_ids.h"
|
|
#include "third_party/blink/public/common/web_preferences/web_preferences.h"
|
|
#include "third_party/blink/public/web/web_local_frame.h"
|
|
#include "third_party/blink/renderer/bindings/core/v8/referrer_script_info.h" // nogncheck
|
|
#include "third_party/blink/renderer/bindings/core/v8/v8_initializer.h" // nogncheck
|
|
#include "third_party/electron_node/src/debug_utils.h"
|
|
#include "third_party/electron_node/src/module_wrap.h"
|
|
#include "v8/include/v8-statistics.h"
|
|
|
|
#if !IS_MAS_BUILD()
|
|
#include "shell/common/crash_keys.h"
|
|
#endif
|
|
|
|
#define ELECTRON_BROWSER_BINDINGS(V) \
|
|
V(electron_browser_app) \
|
|
V(electron_browser_auto_updater) \
|
|
V(electron_browser_content_tracing) \
|
|
V(electron_browser_crash_reporter) \
|
|
V(electron_browser_desktop_capturer) \
|
|
V(electron_browser_dialog) \
|
|
V(electron_browser_event_emitter) \
|
|
V(electron_browser_global_shortcut) \
|
|
V(electron_browser_image_view) \
|
|
V(electron_browser_in_app_purchase) \
|
|
V(electron_browser_menu) \
|
|
V(electron_browser_message_port) \
|
|
V(electron_browser_msix_updater) \
|
|
V(electron_browser_native_theme) \
|
|
V(electron_browser_notification) \
|
|
V(electron_browser_power_monitor) \
|
|
V(electron_browser_power_save_blocker) \
|
|
V(electron_browser_protocol) \
|
|
V(electron_browser_printing) \
|
|
V(electron_browser_push_notifications) \
|
|
V(electron_browser_safe_storage) \
|
|
V(electron_browser_service_worker_main) \
|
|
V(electron_browser_session) \
|
|
V(electron_browser_screen) \
|
|
V(electron_browser_system_preferences) \
|
|
V(electron_browser_base_window) \
|
|
V(electron_browser_tray) \
|
|
V(electron_browser_utility_process) \
|
|
V(electron_browser_view) \
|
|
V(electron_browser_web_contents) \
|
|
V(electron_browser_web_contents_view) \
|
|
V(electron_browser_web_frame_main) \
|
|
V(electron_browser_web_view_manager) \
|
|
V(electron_browser_window) \
|
|
V(electron_common_net)
|
|
|
|
#define ELECTRON_COMMON_BINDINGS(V) \
|
|
V(electron_common_asar) \
|
|
V(electron_common_clipboard) \
|
|
V(electron_common_command_line) \
|
|
V(electron_common_crashpad_support) \
|
|
V(electron_common_environment) \
|
|
V(electron_common_features) \
|
|
V(electron_common_native_image) \
|
|
V(electron_common_shared_texture) \
|
|
V(electron_common_shell) \
|
|
V(electron_common_v8_util)
|
|
|
|
#define ELECTRON_RENDERER_BINDINGS(V) \
|
|
V(electron_renderer_web_utils) \
|
|
V(electron_renderer_context_bridge) \
|
|
V(electron_renderer_crash_reporter) \
|
|
V(electron_renderer_ipc) \
|
|
V(electron_renderer_web_frame)
|
|
|
|
#define ELECTRON_UTILITY_BINDINGS(V) \
|
|
V(electron_browser_event_emitter) \
|
|
V(electron_browser_system_preferences) \
|
|
V(electron_common_net) \
|
|
V(electron_utility_parent_port)
|
|
|
|
#define ELECTRON_TESTING_BINDINGS(V) V(electron_common_testing)
|
|
|
|
// This is used to load built-in bindings. Instead of using
|
|
// __attribute__((constructor)), we call the _register_<modname>
|
|
// function for each built-in bindings explicitly. This is only
|
|
// forward declaration. The definitions are in each binding's
|
|
// implementation when calling the NODE_LINKED_BINDING_CONTEXT_AWARE.
|
|
#define V(modname) void _register_##modname();
|
|
ELECTRON_BROWSER_BINDINGS(V)
|
|
ELECTRON_COMMON_BINDINGS(V)
|
|
ELECTRON_RENDERER_BINDINGS(V)
|
|
ELECTRON_UTILITY_BINDINGS(V)
|
|
#if DCHECK_IS_ON()
|
|
ELECTRON_TESTING_BINDINGS(V)
|
|
#endif
|
|
#undef V
|
|
|
|
using node::loader::ModuleWrap;
|
|
|
|
namespace {
|
|
|
|
void stop_and_close_uv_loop(uv_loop_t* loop) {
|
|
uv_stop(loop);
|
|
|
|
auto const ensure_closing = [](uv_handle_t* handle, void*) {
|
|
// We should be using the UvHandle wrapper everywhere, in which case
|
|
// all handles should already be in a closing state...
|
|
DCHECK(uv_is_closing(handle));
|
|
// ...but if a raw handle got through, through, do the right thing anyway
|
|
if (!uv_is_closing(handle)) {
|
|
uv_close(handle, nullptr);
|
|
}
|
|
};
|
|
|
|
uv_walk(loop, ensure_closing, nullptr);
|
|
|
|
// All remaining handles are in a closing state now.
|
|
// Pump the event loop so that they can finish closing.
|
|
for (;;)
|
|
if (uv_run(loop, UV_RUN_DEFAULT) == 0)
|
|
break;
|
|
|
|
DCHECK_EQ(0, uv_loop_alive(loop));
|
|
node::CheckedUvLoopClose(loop);
|
|
}
|
|
|
|
bool g_is_initialized = false;
|
|
|
|
void V8FatalErrorCallback(const char* location, const char* message) {
|
|
LOG(ERROR) << "Fatal error in V8: " << location << " " << message;
|
|
|
|
#if !IS_MAS_BUILD()
|
|
electron::crash_keys::SetCrashKey("electron.v8-fatal.message", message);
|
|
electron::crash_keys::SetCrashKey("electron.v8-fatal.location", location);
|
|
#endif
|
|
|
|
volatile int* zero = nullptr;
|
|
*zero = 0;
|
|
}
|
|
|
|
void V8OOMErrorCallback(const char* location, const v8::OOMDetails& details) {
|
|
const char* message =
|
|
details.is_heap_oom ? "Allocation failed - JavaScript heap out of memory"
|
|
: "Allocation failed - process out of memory";
|
|
if (location) {
|
|
LOG(ERROR) << "OOM error in V8: " << location << " " << message;
|
|
} else {
|
|
LOG(ERROR) << "OOM error in V8: " << message;
|
|
}
|
|
if (details.detail) {
|
|
LOG(ERROR) << "OOM detail: " << details.detail;
|
|
}
|
|
|
|
#if !IS_MAS_BUILD()
|
|
electron::crash_keys::SetCrashKey("electron.v8-oom.is_heap_oom",
|
|
base::NumberToString(details.is_heap_oom));
|
|
if (location) {
|
|
electron::crash_keys::SetCrashKey("electron.v8-oom.location", location);
|
|
}
|
|
if (details.detail) {
|
|
electron::crash_keys::SetCrashKey("electron.v8-oom.detail", details.detail);
|
|
}
|
|
|
|
// TryGetCurrent() instead of GetCurrent() to avoid FATAL if no isolate.
|
|
v8::Isolate* isolate = v8::Isolate::TryGetCurrent();
|
|
if (isolate) {
|
|
v8::HeapStatistics stats;
|
|
isolate->GetHeapStatistics(&stats);
|
|
electron::crash_keys::SetCrashKey(
|
|
"electron.v8-oom.heap.used",
|
|
base::NumberToString(stats.used_heap_size()));
|
|
electron::crash_keys::SetCrashKey(
|
|
"electron.v8-oom.heap.total",
|
|
base::NumberToString(stats.total_heap_size()));
|
|
electron::crash_keys::SetCrashKey(
|
|
"electron.v8-oom.heap.limit",
|
|
base::NumberToString(stats.heap_size_limit()));
|
|
electron::crash_keys::SetCrashKey(
|
|
"electron.v8-oom.heap.total_available",
|
|
base::NumberToString(stats.total_available_size()));
|
|
}
|
|
#endif
|
|
|
|
OOM_CRASH(0);
|
|
}
|
|
|
|
bool AllowWasmCodeGenerationCallback(v8::Local<v8::Context> context,
|
|
v8::Local<v8::String> source) {
|
|
// If we're running with contextIsolation enabled in the renderer process,
|
|
// fall back to Blink's logic.
|
|
if (node::Environment::GetCurrent(context) == nullptr) {
|
|
if (!electron::IsRendererProcess())
|
|
return false;
|
|
return blink::V8Initializer::WasmCodeGenerationCheckCallback(context,
|
|
source);
|
|
}
|
|
|
|
return node::AllowWasmCodeGenerationCallback(context, source);
|
|
}
|
|
|
|
enum ESMHandlerPlatform {
|
|
kNone,
|
|
kNodeJS,
|
|
kBlink,
|
|
};
|
|
|
|
static ESMHandlerPlatform SelectESMHandlerPlatform(
|
|
v8::Local<v8::Context> context,
|
|
v8::Local<v8::Data> raw_host_defined_options) {
|
|
if (node::Environment::GetCurrent(context) == nullptr) {
|
|
if (electron::IsBrowserProcess() || electron::IsUtilityProcess())
|
|
return ESMHandlerPlatform::kNone;
|
|
|
|
return ESMHandlerPlatform::kBlink;
|
|
}
|
|
|
|
if (!electron::IsRendererProcess())
|
|
return ESMHandlerPlatform::kNodeJS;
|
|
|
|
blink::WebLocalFrame* frame = blink::WebLocalFrame::FrameForContext(context);
|
|
|
|
if (frame == nullptr)
|
|
return ESMHandlerPlatform::kBlink;
|
|
|
|
auto prefs = content::RenderFrame::FromWebFrame(frame)->GetBlinkPreferences();
|
|
|
|
// If we're running with contextIsolation enabled in the renderer process,
|
|
// fall back to Blink's logic when the frame is not in the isolated world.
|
|
if (prefs.context_isolation) {
|
|
return frame->GetScriptContextWorldId(context) ==
|
|
electron::WorldIDs::ISOLATED_WORLD_ID
|
|
? ESMHandlerPlatform::kNodeJS
|
|
: ESMHandlerPlatform::kBlink;
|
|
}
|
|
|
|
if (raw_host_defined_options.IsEmpty() ||
|
|
!raw_host_defined_options->IsFixedArray()) {
|
|
return ESMHandlerPlatform::kBlink;
|
|
}
|
|
|
|
// Since the routing is based on the `host_defined_options` length -
|
|
// make sure that Node's host defined options are different from Blink's.
|
|
static_assert(
|
|
static_cast<size_t>(node::loader::HostDefinedOptions::kLength) !=
|
|
blink::ReferrerScriptInfo::HostDefinedOptionsIndex::kLength);
|
|
|
|
// Use Node.js resolver only if host options were created by it.
|
|
auto options = v8::Local<v8::FixedArray>::Cast(raw_host_defined_options);
|
|
if (options->Length() == node::loader::HostDefinedOptions::kLength) {
|
|
return ESMHandlerPlatform::kNodeJS;
|
|
}
|
|
|
|
return ESMHandlerPlatform::kBlink;
|
|
}
|
|
|
|
v8::MaybeLocal<v8::Promise> HostImportModuleWithPhaseDynamically(
|
|
v8::Local<v8::Context> context,
|
|
v8::Local<v8::Data> v8_host_defined_options,
|
|
v8::Local<v8::Value> v8_referrer_resource_url,
|
|
v8::Local<v8::String> v8_specifier,
|
|
v8::ModuleImportPhase import_phase,
|
|
v8::Local<v8::FixedArray> v8_import_attributes) {
|
|
switch (SelectESMHandlerPlatform(context, v8_host_defined_options)) {
|
|
case ESMHandlerPlatform::kBlink:
|
|
return blink::V8Initializer::HostImportModuleWithPhaseDynamically(
|
|
context, v8_host_defined_options, v8_referrer_resource_url,
|
|
v8_specifier, import_phase, v8_import_attributes);
|
|
case ESMHandlerPlatform::kNodeJS:
|
|
CHECK(import_phase == v8::ModuleImportPhase::kEvaluation);
|
|
return node::loader::ImportModuleDynamicallyWithPhase(
|
|
context, v8_host_defined_options, v8_referrer_resource_url,
|
|
v8_specifier, import_phase, v8_import_attributes);
|
|
case ESMHandlerPlatform::kNone:
|
|
default:
|
|
return {};
|
|
}
|
|
}
|
|
|
|
v8::MaybeLocal<v8::Promise> HostImportModuleDynamically(
|
|
v8::Local<v8::Context> context,
|
|
v8::Local<v8::Data> v8_host_defined_options,
|
|
v8::Local<v8::Value> v8_referrer_resource_url,
|
|
v8::Local<v8::String> v8_specifier,
|
|
v8::Local<v8::FixedArray> v8_import_attributes) {
|
|
return HostImportModuleWithPhaseDynamically(
|
|
context, v8_host_defined_options, v8_referrer_resource_url, v8_specifier,
|
|
v8::ModuleImportPhase::kEvaluation, v8_import_attributes);
|
|
}
|
|
|
|
void HostInitializeImportMetaObject(v8::Local<v8::Context> context,
|
|
v8::Local<v8::Module> module,
|
|
v8::Local<v8::Object> meta) {
|
|
node::Environment* env = node::Environment::GetCurrent(context);
|
|
if (env == nullptr) {
|
|
if (electron::IsBrowserProcess() || electron::IsUtilityProcess())
|
|
return;
|
|
return blink::V8Initializer::HostGetImportMetaProperties(context, module,
|
|
meta);
|
|
}
|
|
|
|
if (electron::IsRendererProcess()) {
|
|
// If the module is created by Node.js, use Node.js' handling.
|
|
if (env != nullptr) {
|
|
ModuleWrap* wrap = ModuleWrap::GetFromModule(env, module);
|
|
if (wrap)
|
|
return ModuleWrap::HostInitializeImportMetaObjectCallback(context,
|
|
module, meta);
|
|
}
|
|
|
|
// If contextIsolation is enabled, fall back to Blink's handling.
|
|
blink::WebLocalFrame* frame =
|
|
blink::WebLocalFrame::FrameForContext(context);
|
|
if (!frame || frame->GetScriptContextWorldId(context) !=
|
|
electron::WorldIDs::ISOLATED_WORLD_ID) {
|
|
return blink::V8Initializer::HostGetImportMetaProperties(context, module,
|
|
meta);
|
|
}
|
|
}
|
|
|
|
return ModuleWrap::HostInitializeImportMetaObjectCallback(context, module,
|
|
meta);
|
|
}
|
|
|
|
v8::ModifyCodeGenerationFromStringsResult ModifyCodeGenerationFromStrings(
|
|
v8::Local<v8::Context> context,
|
|
v8::Local<v8::Value> source,
|
|
bool is_code_like) {
|
|
if (node::Environment::GetCurrent(context) == nullptr) {
|
|
// No node environment means we're in the renderer process, either in a
|
|
// sandboxed renderer or in an unsandboxed renderer with context isolation
|
|
// enabled.
|
|
if (!electron::IsRendererProcess()) {
|
|
NOTREACHED();
|
|
}
|
|
return blink::V8Initializer::CodeGenerationCheckCallbackInMainThread(
|
|
context, source, is_code_like);
|
|
}
|
|
|
|
// If we get here then we have a node environment, so either a) we're in the
|
|
// non-renderer process, or b) we're in the renderer process in a context that
|
|
// has both node and blink, i.e. contextIsolation disabled.
|
|
|
|
// If we're in the renderer with contextIsolation disabled, ask blink first
|
|
// (for CSP), and iff that allows codegen, delegate to node.
|
|
if (electron::IsRendererProcess()) {
|
|
v8::ModifyCodeGenerationFromStringsResult result =
|
|
blink::V8Initializer::CodeGenerationCheckCallbackInMainThread(
|
|
context, source, is_code_like);
|
|
if (!result.codegen_allowed)
|
|
return result;
|
|
}
|
|
|
|
// If we're in the main process or utility process, delegate to node.
|
|
return node::ModifyCodeGenerationFromStrings(context, source, is_code_like);
|
|
}
|
|
|
|
void ErrorMessageListener(v8::Local<v8::Message> message,
|
|
v8::Local<v8::Value> data) {
|
|
v8::Isolate* isolate = v8::Isolate::GetCurrent();
|
|
node::Environment* env = node::Environment::GetCurrent(isolate);
|
|
if (env) {
|
|
v8::MicrotasksScope microtasks_scope(
|
|
env->context(), v8::MicrotasksScope::kDoNotRunMicrotasks);
|
|
// Emit the after() hooks now that the exception has been handled.
|
|
// Analogous to node/lib/internal/process/execution.js#L176-L180
|
|
if (env->async_hooks()->fields()[node::AsyncHooks::kAfter]) {
|
|
while (env->async_hooks()->fields()[node::AsyncHooks::kStackLength]) {
|
|
double id = env->execution_async_id();
|
|
// Do not call EmitAfter for asyncId 0.
|
|
if (id != 0)
|
|
node::AsyncWrap::EmitAfter(env, id);
|
|
env->async_hooks()->pop_async_context(id);
|
|
}
|
|
}
|
|
|
|
// Ensure that the async id stack is properly cleared so the async
|
|
// hook stack does not become corrupted.
|
|
env->async_hooks()->clear_async_id_stack();
|
|
}
|
|
}
|
|
|
|
// Only allow a specific subset of options in non-ELECTRON_RUN_AS_NODE mode.
|
|
// If node CLI inspect support is disabled, allow no debug options.
|
|
bool IsAllowedOption(const std::string_view option) {
|
|
static constexpr auto debug_options =
|
|
base::MakeFixedFlatSet<std::string_view>({
|
|
"--debug",
|
|
"--debug-brk",
|
|
"--debug-port",
|
|
"--inspect",
|
|
"--inspect-brk",
|
|
"--inspect-brk-node",
|
|
"--inspect-port",
|
|
"--inspect-publish-uid",
|
|
"--experimental-network-inspection",
|
|
"--experimental-transform-types",
|
|
});
|
|
|
|
// This should be aligned with what's possible to set via the process object.
|
|
static constexpr auto options = base::MakeFixedFlatSet<std::string_view>({
|
|
"--diagnostic-dir",
|
|
"--dns-result-order",
|
|
"--no-deprecation",
|
|
"--throw-deprecation",
|
|
"--trace-deprecation",
|
|
"--trace-warnings",
|
|
"--no-experimental-global-navigator",
|
|
});
|
|
|
|
// This should be aligned with what's possible to set via the process object.
|
|
static constexpr auto unpacked_options =
|
|
base::MakeFixedFlatSet<std::string_view>({
|
|
"--expose-internals",
|
|
});
|
|
|
|
if (debug_options.contains(option))
|
|
return electron::fuses::IsNodeCliInspectEnabled();
|
|
|
|
if (unpacked_options.contains(option))
|
|
return !electron::api::App::IsPackaged();
|
|
|
|
return options.contains(option);
|
|
}
|
|
|
|
// Initialize NODE_OPTIONS to pass to Node.js
|
|
// See https://nodejs.org/api/cli.html#cli_node_options_options
|
|
void SetNodeOptions(base::Environment* env) {
|
|
// Options that are expressly disallowed
|
|
static constexpr auto disallowed = base::MakeFixedFlatSet<std::string_view>({
|
|
"--enable-fips",
|
|
"--experimental-policy",
|
|
"--force-fips",
|
|
"--openssl-config",
|
|
"--use-bundled-ca",
|
|
"--use-openssl-ca",
|
|
});
|
|
|
|
static constexpr auto pkg_opts = base::MakeFixedFlatSet<std::string_view>({
|
|
"--http-parser",
|
|
"--max-http-header-size",
|
|
});
|
|
|
|
if (env->HasVar("NODE_EXTRA_CA_CERTS")) {
|
|
if (!electron::fuses::IsNodeOptionsEnabled()) {
|
|
LOG(WARNING) << "NODE_OPTIONS ignored due to disabled nodeOptions fuse.";
|
|
env->UnSetVar("NODE_EXTRA_CA_CERTS");
|
|
}
|
|
}
|
|
|
|
if (env->HasVar("NODE_OPTIONS")) {
|
|
if (electron::fuses::IsNodeOptionsEnabled()) {
|
|
std::string result_options;
|
|
std::string options = env->GetVar("NODE_OPTIONS").value();
|
|
const std::vector<std::string_view> parts = base::SplitStringPiece(
|
|
options, " ", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
|
|
|
|
bool is_packaged_app = electron::api::App::IsPackaged();
|
|
|
|
for (const std::string_view part : parts) {
|
|
// Strip off values passed to individual NODE_OPTIONs
|
|
const std::string_view option = part.substr(0, part.find('='));
|
|
|
|
if (is_packaged_app && !pkg_opts.contains(option)) {
|
|
// Explicitly disallow majority of NODE_OPTIONS in packaged apps
|
|
LOG(ERROR) << "Most NODE_OPTIONs are not supported in packaged apps."
|
|
<< " See documentation for more details.";
|
|
continue;
|
|
} else if (disallowed.contains(option)) {
|
|
// Remove NODE_OPTIONS specifically disallowed for use in Node.js
|
|
// through Electron owing to constraints like BoringSSL.
|
|
LOG(ERROR) << "The NODE_OPTION " << option
|
|
<< " is not supported in Electron";
|
|
continue;
|
|
}
|
|
result_options.append(part);
|
|
result_options.append(" ");
|
|
}
|
|
|
|
// overwrite new NODE_OPTIONS without unsupported variables
|
|
env->SetVar("NODE_OPTIONS", result_options);
|
|
} else {
|
|
LOG(WARNING) << "NODE_OPTIONS ignored due to disabled nodeOptions fuse.";
|
|
env->UnSetVar("NODE_OPTIONS");
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
namespace electron {
|
|
|
|
namespace {
|
|
|
|
base::FilePath GetResourcesPath() {
|
|
#if BUILDFLAG(IS_MAC)
|
|
return MainApplicationBundlePath().Append("Contents").Append("Resources");
|
|
#else
|
|
base::FilePath assets_path;
|
|
base::PathService::Get(base::DIR_ASSETS, &assets_path);
|
|
|
|
return assets_path.Append(FILE_PATH_LITERAL("resources"));
|
|
#endif
|
|
}
|
|
} // namespace
|
|
|
|
NodeBindings::NodeBindings(BrowserEnvironment browser_env)
|
|
: browser_env_{browser_env},
|
|
uv_loop_{InitEventLoop(browser_env, &worker_loop_)} {}
|
|
|
|
NodeBindings::~NodeBindings() {
|
|
StopPolling();
|
|
|
|
// Clear uv.
|
|
uv_sem_destroy(&embed_sem_);
|
|
dummy_uv_handle_.reset();
|
|
|
|
// Clean up worker loop
|
|
if (in_worker_loop())
|
|
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_);
|
|
|
|
// Drain any leftover semaphore posts so the next embed thread starts
|
|
// in a clean lock-step state. A stale count can occur if UvRunOnce
|
|
// posted sem_post concurrently with our StopPolling sem_post — the
|
|
// embed thread consumed one to see embed_closed_ and exited, leaving
|
|
// the other unconsumed. Without draining, the next EmbedThreadRunner
|
|
// would fall through uv_sem_wait immediately, breaking the intended
|
|
// one-post-per-UvRunOnce protocol.
|
|
while (uv_sem_trywait(&embed_sem_) == 0) {
|
|
}
|
|
|
|
// Allow PrepareEmbedThread + StartPolling to restart.
|
|
embed_closed_ = false;
|
|
initialized_ = false;
|
|
}
|
|
|
|
node::IsolateData* NodeBindings::isolate_data(
|
|
v8::Local<v8::Context> context) const {
|
|
if (context->GetNumberOfEmbedderDataFields() <=
|
|
kElectronContextEmbedderDataIndex) {
|
|
return nullptr;
|
|
}
|
|
auto* isolate_data = static_cast<node::IsolateData*>(
|
|
context->GetAlignedPointerFromEmbedderData(
|
|
kElectronContextEmbedderDataIndex, v8::kEmbedderDataTypeTagDefault));
|
|
CHECK(isolate_data);
|
|
CHECK(isolate_data->event_loop());
|
|
return isolate_data;
|
|
}
|
|
|
|
// static
|
|
uv_loop_t* NodeBindings::InitEventLoop(BrowserEnvironment browser_env,
|
|
uv_loop_t* worker_loop) {
|
|
uv_loop_t* event_loop = nullptr;
|
|
|
|
if (browser_env == BrowserEnvironment::kWorker) {
|
|
uv_loop_init(worker_loop);
|
|
event_loop = worker_loop;
|
|
} else {
|
|
event_loop = uv_default_loop();
|
|
}
|
|
|
|
// Interrupt embed polling when a handle is started.
|
|
uv_loop_configure(event_loop, UV_LOOP_INTERRUPT_ON_IO_CHANGE);
|
|
|
|
return event_loop;
|
|
}
|
|
|
|
void NodeBindings::RegisterBuiltinBindings() {
|
|
#define V(modname) _register_##modname();
|
|
if (IsBrowserProcess()) {
|
|
ELECTRON_BROWSER_BINDINGS(V)
|
|
}
|
|
ELECTRON_COMMON_BINDINGS(V)
|
|
if (IsRendererProcess()) {
|
|
ELECTRON_RENDERER_BINDINGS(V)
|
|
}
|
|
if (IsUtilityProcess()) {
|
|
ELECTRON_UTILITY_BINDINGS(V)
|
|
}
|
|
#if DCHECK_IS_ON()
|
|
ELECTRON_TESTING_BINDINGS(V)
|
|
#endif
|
|
#undef V
|
|
}
|
|
|
|
bool NodeBindings::IsInitialized() {
|
|
return g_is_initialized;
|
|
}
|
|
|
|
// Initialize Node.js cli options to pass to Node.js
|
|
// See https://nodejs.org/api/cli.html#cli_options
|
|
std::vector<std::string> NodeBindings::ParseNodeCliFlags() {
|
|
const auto argv = base::CommandLine::ForCurrentProcess()->argv();
|
|
std::vector<std::string> args;
|
|
|
|
// TODO(codebytere): We need to set the first entry in args to the
|
|
// process name owing to src/node_options-inl.h#L286-L290 but this is
|
|
// redundant and so should be refactored upstream.
|
|
args.reserve(argv.size() + 1);
|
|
args.emplace_back("electron");
|
|
|
|
for (const auto& arg : argv) {
|
|
#if BUILDFLAG(IS_WIN)
|
|
const auto& option = base::WideToUTF8(arg);
|
|
#else
|
|
const auto& option = arg;
|
|
#endif
|
|
const auto stripped = std::string_view{option}.substr(0, option.find('='));
|
|
// Only allow no-op or a small set of debug/trace related options.
|
|
if (IsAllowedOption(stripped) || stripped == "--")
|
|
args.push_back(option);
|
|
}
|
|
|
|
// We need to disable Node.js' fetch implementation to prevent
|
|
// conflict with Blink's in renderer and worker processes.
|
|
if (browser_env_ == BrowserEnvironment::kRenderer ||
|
|
browser_env_ == BrowserEnvironment::kWorker) {
|
|
args.push_back("--no-experimental-fetch");
|
|
}
|
|
|
|
return args;
|
|
}
|
|
|
|
void NodeBindings::Initialize(v8::Isolate* const isolate,
|
|
v8::Local<v8::Context> context) {
|
|
TRACE_EVENT0("electron", "NodeBindings::Initialize");
|
|
// Open node's error reporting system for browser process.
|
|
|
|
#if BUILDFLAG(IS_LINUX)
|
|
// Get real command line in renderer process forked by zygote.
|
|
if (browser_env_ != BrowserEnvironment::kBrowser)
|
|
ElectronCommandLine::InitializeFromCommandLine();
|
|
#endif
|
|
|
|
// Explicitly register electron's builtin bindings.
|
|
RegisterBuiltinBindings();
|
|
|
|
auto env = base::Environment::Create();
|
|
SetNodeOptions(env.get());
|
|
|
|
// Parse and set Node.js cli flags.
|
|
std::vector<std::string> args = ParseNodeCliFlags();
|
|
|
|
// V8::EnableWebAssemblyTrapHandler can be called only once or it will
|
|
// hard crash. We need to prevent Node.js calling it in the event it has
|
|
// already been called.
|
|
node::per_process::cli_options->disable_wasm_trap_handler = true;
|
|
|
|
uint64_t process_flags =
|
|
node::ProcessInitializationFlags::kNoInitializeCppgc |
|
|
node::ProcessInitializationFlags::kNoInitializeV8 |
|
|
node::ProcessInitializationFlags::kNoInitializeNodeV8Platform;
|
|
|
|
// We do not want the child processes spawned from the utility process
|
|
// to inherit the custom stdio handles created for the parent.
|
|
if (browser_env_ != BrowserEnvironment::kUtility)
|
|
process_flags |= node::ProcessInitializationFlags::kEnableStdioInheritance;
|
|
|
|
if (browser_env_ == BrowserEnvironment::kRenderer)
|
|
process_flags |= node::ProcessInitializationFlags::kNoDefaultSignalHandling;
|
|
|
|
if (!fuses::IsNodeOptionsEnabled())
|
|
process_flags |= node::ProcessInitializationFlags::kDisableNodeOptionsEnv;
|
|
|
|
base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
|
|
if (command_line->HasSwitch(switches::kNoStdioInit)) {
|
|
process_flags |= node::ProcessInitializationFlags::kNoStdioInitialization;
|
|
} else {
|
|
#if BUILDFLAG(IS_WIN)
|
|
if (!platform_util::IsNulDeviceEnabled()) {
|
|
LOG(FATAL) << "Unable to open nul device needed for initialization,"
|
|
"aborting startup. As a workaround, try starting with --"
|
|
<< switches::kNoStdioInit;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
std::shared_ptr<node::InitializationResult> result =
|
|
node::InitializeOncePerProcess(
|
|
args,
|
|
static_cast<node::ProcessInitializationFlags::Flags>(process_flags));
|
|
|
|
for (const std::string& error : result->errors())
|
|
std::cerr << args[0] << ": " << error << '\n';
|
|
|
|
if (result->early_return() != 0)
|
|
exit(result->exit_code());
|
|
|
|
#if BUILDFLAG(IS_WIN)
|
|
// uv_init overrides error mode to suppress the default crash dialog, bring
|
|
// it back if user wants to show it.
|
|
if (browser_env_ == BrowserEnvironment::kBrowser ||
|
|
env->HasVar("ELECTRON_DEFAULT_ERROR_MODE"))
|
|
SetErrorMode(GetErrorMode() & ~SEM_NOGPFAULTERRORBOX);
|
|
#endif
|
|
|
|
gin_helper::internal::Event::GetConstructor(
|
|
isolate, context, &gin_helper::internal::Event::kWrapperInfo);
|
|
|
|
g_is_initialized = true;
|
|
}
|
|
|
|
std::shared_ptr<node::Environment> NodeBindings::CreateEnvironment(
|
|
v8::Isolate* isolate,
|
|
v8::Local<v8::Context> context,
|
|
node::MultiIsolatePlatform* platform,
|
|
size_t max_young_generation_size,
|
|
std::vector<std::string> args,
|
|
std::vector<std::string> exec_args,
|
|
std::optional<base::RepeatingCallback<void()>> on_app_code_ready) {
|
|
// Feed node the path to initialization script.
|
|
std::string process_type;
|
|
switch (browser_env_) {
|
|
case BrowserEnvironment::kBrowser:
|
|
process_type = "browser";
|
|
break;
|
|
case BrowserEnvironment::kRenderer:
|
|
process_type = "renderer";
|
|
break;
|
|
case BrowserEnvironment::kWorker:
|
|
process_type = "worker";
|
|
break;
|
|
case BrowserEnvironment::kUtility:
|
|
process_type = "utility";
|
|
break;
|
|
}
|
|
|
|
gin_helper::Dictionary global(isolate, context->Global());
|
|
|
|
if (browser_env_ == BrowserEnvironment::kBrowser) {
|
|
const std::vector<std::string> search_paths = {"app.asar", "app",
|
|
"default_app.asar"};
|
|
const std::vector<std::string> app_asar_search_paths = {"app.asar"};
|
|
context->Global()->SetPrivate(
|
|
context,
|
|
v8::Private::ForApi(
|
|
isolate,
|
|
gin::ConvertToV8(isolate, "appSearchPaths").As<v8::String>()),
|
|
gin::ConvertToV8(isolate,
|
|
electron::fuses::IsOnlyLoadAppFromAsarEnabled()
|
|
? app_asar_search_paths
|
|
: search_paths));
|
|
context->Global()->SetPrivate(
|
|
context,
|
|
v8::Private::ForApi(
|
|
isolate, gin::ConvertToV8(isolate, "appSearchPathsOnlyLoadASAR")
|
|
.As<v8::String>()),
|
|
gin::ConvertToV8(isolate,
|
|
electron::fuses::IsOnlyLoadAppFromAsarEnabled()));
|
|
}
|
|
|
|
std::string init_script = "electron/js2c/" + process_type + "_init";
|
|
|
|
args.insert(args.begin() + 1, init_script);
|
|
|
|
auto* isolate_data = node::CreateIsolateData(isolate, uv_loop_, platform);
|
|
isolate_data->max_young_gen_size = max_young_generation_size;
|
|
context->SetAlignedPointerInEmbedderData(kElectronContextEmbedderDataIndex,
|
|
static_cast<void*>(isolate_data),
|
|
v8::kEmbedderDataTypeTagDefault);
|
|
|
|
uint64_t env_flags = node::EnvironmentFlags::kDefaultFlags |
|
|
node::EnvironmentFlags::kHideConsoleWindows |
|
|
node::EnvironmentFlags::kNoGlobalSearchPaths |
|
|
node::EnvironmentFlags::kNoRegisterESMLoader;
|
|
|
|
if (browser_env_ == BrowserEnvironment::kRenderer ||
|
|
browser_env_ == BrowserEnvironment::kWorker) {
|
|
// Only one ESM loader can be registered per isolate -
|
|
// in renderer processes this should be blink. We need to tell Node.js
|
|
// not to register its handler (overriding blinks) in non-browser processes.
|
|
// We also avoid overriding globals like setImmediate, clearImmediate
|
|
// queueMicrotask etc during the bootstrap phase of Node.js
|
|
// for processes that already have these defined by DOM.
|
|
// Check //third_party/electron_node/lib/internal/bootstrap/node.js
|
|
// for the list of overrides on globalThis.
|
|
env_flags |= node::EnvironmentFlags::kNoBrowserGlobals |
|
|
node::EnvironmentFlags::kNoCreateInspector;
|
|
}
|
|
|
|
if (!electron::fuses::IsNodeCliInspectEnabled()) {
|
|
// If --inspect and friends are disabled we also shouldn't listen for
|
|
// SIGUSR1
|
|
env_flags |= node::EnvironmentFlags::kNoStartDebugSignalHandler;
|
|
}
|
|
|
|
node::Environment* env = electron::util::CreateEnvironment(
|
|
isolate, static_cast<node::IsolateData*>(isolate_data), context, args,
|
|
exec_args, static_cast<node::EnvironmentFlags::Flags>(env_flags),
|
|
process_type);
|
|
DCHECK(env);
|
|
|
|
node::IsolateSettings is;
|
|
|
|
// Use a custom fatal error callback to allow us to add
|
|
// crash message and location to CrashReports.
|
|
is.fatal_error_callback = V8FatalErrorCallback;
|
|
is.oom_error_callback = V8OOMErrorCallback;
|
|
|
|
// We don't want to abort either in the renderer or browser processes.
|
|
// We already listen for uncaught exceptions and handle them there.
|
|
// For utility process we expect the process to behave as standard
|
|
// Node.js runtime and abort the process with appropriate exit
|
|
// code depending on a handler being set for `uncaughtException` event.
|
|
if (browser_env_ != BrowserEnvironment::kUtility) {
|
|
is.should_abort_on_uncaught_exception_callback = [](v8::Isolate*) {
|
|
return false;
|
|
};
|
|
}
|
|
|
|
// Use a custom callback here to allow us to leverage Blink's logic in the
|
|
// renderer process.
|
|
is.allow_wasm_code_generation_callback = AllowWasmCodeGenerationCallback;
|
|
is.flags |= node::IsolateSettingsFlags::
|
|
ALLOW_MODIFY_CODE_GENERATION_FROM_STRINGS_CALLBACK;
|
|
is.modify_code_generation_from_strings_callback =
|
|
ModifyCodeGenerationFromStrings;
|
|
|
|
if (browser_env_ == BrowserEnvironment::kBrowser ||
|
|
browser_env_ == BrowserEnvironment::kUtility) {
|
|
// Node.js requires that microtask checkpoints be explicitly invoked.
|
|
is.policy = v8::MicrotasksPolicy::kExplicit;
|
|
} else {
|
|
// Blink expects the microtasks policy to be kScoped, but Node.js expects it
|
|
// to be kExplicit. In the renderer, there can be many contexts within the
|
|
// same isolate, so we don't want to change the existing policy here, which
|
|
// could be either kExplicit or kScoped depending on whether we're executing
|
|
// from within a Node.js or a Blink entrypoint. Instead, the policy is
|
|
// toggled to kExplicit when entering Node.js through UvRunOnce.
|
|
is.policy = isolate->GetMicrotasksPolicy();
|
|
|
|
// We do not want to use Node.js' message listener as it interferes with
|
|
// Blink's.
|
|
is.flags &= ~node::IsolateSettingsFlags::MESSAGE_LISTENER_WITH_ERROR_LEVEL;
|
|
|
|
// Isolate message listeners are additive (you can add multiple), so instead
|
|
// we add an extra one here to ensure that the async hook stack is properly
|
|
// cleared when errors are thrown.
|
|
isolate->AddMessageListenerWithErrorLevel(ErrorMessageListener,
|
|
v8::Isolate::kMessageError);
|
|
|
|
// We do not want to use the promise rejection callback that Node.js uses,
|
|
// because it does not send PromiseRejectionEvents to the global script
|
|
// context. We need to use the one Blink already provides.
|
|
is.flags |=
|
|
node::IsolateSettingsFlags::SHOULD_NOT_SET_PROMISE_REJECTION_CALLBACK;
|
|
|
|
// We do not want to use the stack trace callback that Node.js uses,
|
|
// because it relies on Node.js being aware of the current Context and
|
|
// that's not always the case. We need to use the one Blink already
|
|
// provides.
|
|
is.flags |=
|
|
node::IsolateSettingsFlags::SHOULD_NOT_SET_PREPARE_STACK_TRACE_CALLBACK;
|
|
}
|
|
|
|
node::SetIsolateUpForNode(isolate, is);
|
|
isolate->SetHostImportModuleDynamicallyCallback(HostImportModuleDynamically);
|
|
isolate->SetHostImportModuleWithPhaseDynamicallyCallback(
|
|
HostImportModuleWithPhaseDynamically);
|
|
isolate->SetHostInitializeImportMetaObjectCallback(
|
|
HostInitializeImportMetaObject);
|
|
|
|
gin_helper::Dictionary process(isolate, env->process_object());
|
|
process.SetReadOnly("type", process_type);
|
|
|
|
if (browser_env_ == BrowserEnvironment::kBrowser ||
|
|
browser_env_ == BrowserEnvironment::kRenderer) {
|
|
if (on_app_code_ready) {
|
|
process.SetMethod("appCodeLoaded", std::move(*on_app_code_ready));
|
|
} else {
|
|
process.SetMethod("appCodeLoaded",
|
|
base::BindRepeating(&NodeBindings::SetAppCodeLoaded,
|
|
base::Unretained(this)));
|
|
}
|
|
}
|
|
|
|
auto env_deleter = [isolate, isolate_data,
|
|
context = v8::Global<v8::Context>{isolate, context}](
|
|
node::Environment* nenv) mutable {
|
|
// When `isolate_data` was created above, a pointer to it was kept
|
|
// in context's embedder_data[kElectronContextEmbedderDataIndex].
|
|
// Since we're about to free `isolate_data`, clear that entry
|
|
v8::HandleScope handle_scope{isolate};
|
|
context.Get(isolate)->SetAlignedPointerInEmbedderData(
|
|
kElectronContextEmbedderDataIndex, nullptr,
|
|
v8::kEmbedderDataTypeTagDefault);
|
|
context.Reset();
|
|
|
|
node::FreeEnvironment(nenv);
|
|
node::FreeIsolateData(isolate_data);
|
|
};
|
|
|
|
return {env, std::move(env_deleter)};
|
|
}
|
|
|
|
std::shared_ptr<node::Environment> NodeBindings::CreateEnvironment(
|
|
v8::Isolate* const isolate,
|
|
v8::Local<v8::Context> context,
|
|
node::MultiIsolatePlatform* platform,
|
|
size_t max_young_generation_size,
|
|
std::optional<base::RepeatingCallback<void()>> on_app_code_ready) {
|
|
return CreateEnvironment(
|
|
isolate, context, platform, max_young_generation_size,
|
|
ElectronCommandLine::AsUtf8(), {}, on_app_code_ready);
|
|
}
|
|
|
|
void NodeBindings::LoadEnvironment(node::Environment* env) {
|
|
node::LoadEnvironment(env, node::StartExecutionCallback{}, &OnNodePreload);
|
|
gin_helper::EmitEvent(env->isolate(), env->process_object(), "loaded");
|
|
}
|
|
|
|
void NodeBindings::PrepareEmbedThread() {
|
|
// IOCP does not change for the process until the loop is recreated,
|
|
// we ensure that there is only a single polling thread satisfying
|
|
// the concurrency limit set from CreateIoCompletionPort call by
|
|
// uv_loop_init for the lifetime of this process.
|
|
// More background can be found at:
|
|
// https://github.com/microsoft/vscode/issues/142786#issuecomment-1061673400
|
|
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;
|
|
}
|
|
|
|
uv_thread_create(&embed_thread_, EmbedThreadRunner, this);
|
|
}
|
|
|
|
void NodeBindings::StartPolling() {
|
|
// Avoid calling UvRunOnce if the loop is already active,
|
|
// otherwise it can lead to situations were the number of active
|
|
// threads processing on IOCP is greater than the concurrency limit.
|
|
if (initialized_)
|
|
return;
|
|
|
|
initialized_ = true;
|
|
|
|
// The MessageLoop should have been created, remember the one in main thread.
|
|
task_runner_ = base::SingleThreadTaskRunner::GetCurrentDefault();
|
|
|
|
// Run uv loop for once to give the uv__io_poll a chance to add all events.
|
|
UvRunOnce();
|
|
}
|
|
|
|
void NodeBindings::SetAppCodeLoaded() {
|
|
app_code_loaded_ = true;
|
|
}
|
|
|
|
void NodeBindings::JoinAppCode() {
|
|
// We can only "join" app code to the main thread in the browser process
|
|
if (browser_env_ != BrowserEnvironment::kBrowser) {
|
|
return;
|
|
}
|
|
|
|
auto* browser = Browser::Get();
|
|
node::Environment* env = uv_env();
|
|
|
|
if (!env)
|
|
return;
|
|
|
|
v8::HandleScope handle_scope(env->isolate());
|
|
// Enter node context while dealing with uv events.
|
|
v8::Context::Scope context_scope(env->context());
|
|
|
|
// Pump the event loop until we get the signal that the app code has finished
|
|
// loading
|
|
while (!app_code_loaded_ && !browser->is_shutting_down()) {
|
|
int r = uv_run(uv_loop_, UV_RUN_ONCE);
|
|
if (r == 0) {
|
|
base::RunLoop().QuitWhenIdle(); // Quit from uv.
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void NodeBindings::UvRunOnce() {
|
|
node::Environment* env = uv_env();
|
|
|
|
// When doing navigation without restarting renderer process, it may happen
|
|
// that the node environment is destroyed but the message loop is still there.
|
|
// In this case we should not run uv loop.
|
|
if (!env)
|
|
return;
|
|
|
|
v8::HandleScope handle_scope(env->isolate());
|
|
|
|
// Enter node context while dealing with uv events.
|
|
v8::Context::Scope context_scope(env->context());
|
|
|
|
{
|
|
util::ExplicitMicrotasksScope microtasks_scope(
|
|
env->context()->GetMicrotaskQueue());
|
|
|
|
if (browser_env_ != BrowserEnvironment::kBrowser)
|
|
TRACE_EVENT_BEGIN0("devtools.timeline", "FunctionCall");
|
|
|
|
// Deal with uv events.
|
|
int r = uv_run(uv_loop_, UV_RUN_NOWAIT);
|
|
|
|
if (browser_env_ != BrowserEnvironment::kBrowser)
|
|
TRACE_EVENT_END0("devtools.timeline", "FunctionCall");
|
|
|
|
if (r == 0)
|
|
base::RunLoop().QuitWhenIdle(); // Quit from uv.
|
|
}
|
|
|
|
// Tell the worker thread to continue polling.
|
|
uv_sem_post(&embed_sem_);
|
|
}
|
|
|
|
void NodeBindings::WakeupMainThread() {
|
|
DCHECK(task_runner_);
|
|
task_runner_->PostTask(FROM_HERE, base::BindOnce(&NodeBindings::UvRunOnce,
|
|
weak_factory_.GetWeakPtr()));
|
|
}
|
|
|
|
void NodeBindings::WakeupEmbedThread() {
|
|
uv_async_send(dummy_uv_handle_.get());
|
|
}
|
|
|
|
// static
|
|
void NodeBindings::EmbedThreadRunner(void* arg) {
|
|
auto* self = static_cast<NodeBindings*>(arg);
|
|
|
|
while (true) {
|
|
// Wait for the main loop to deal with events.
|
|
uv_sem_wait(&self->embed_sem_);
|
|
if (self->embed_closed_)
|
|
break;
|
|
|
|
// Wait for something to happen in uv loop.
|
|
// Note that the PollEvents() is implemented by derived classes, so when
|
|
// this class is being destructed the PollEvents() would not be available
|
|
// anymore. Because of it we must make sure we only invoke PollEvents()
|
|
// when this class is alive.
|
|
self->PollEvents();
|
|
if (self->embed_closed_)
|
|
break;
|
|
|
|
// Deal with event in main thread.
|
|
self->WakeupMainThread();
|
|
}
|
|
}
|
|
|
|
void OnNodePreload(node::Environment* env,
|
|
v8::Local<v8::Value> process,
|
|
v8::Local<v8::Value> require) {
|
|
// Set custom process properties.
|
|
gin_helper::Dictionary dict(env->isolate(), process.As<v8::Object>());
|
|
dict.SetReadOnly("resourcesPath", GetResourcesPath());
|
|
base::FilePath helper_exec_path; // path to the helper app.
|
|
base::PathService::Get(content::CHILD_PROCESS_EXE, &helper_exec_path);
|
|
dict.SetReadOnly("helperExecPath", helper_exec_path);
|
|
gin_helper::Dictionary versions;
|
|
if (dict.Get("versions", &versions)) {
|
|
versions.SetReadOnly(ELECTRON_PROJECT_NAME, ELECTRON_VERSION_STRING);
|
|
versions.SetReadOnly("chrome", CHROME_VERSION_STRING);
|
|
#if BUILDFLAG(HAS_VENDOR_VERSION)
|
|
versions.SetReadOnly(BUILDFLAG(VENDOR_VERSION_NAME),
|
|
BUILDFLAG(VENDOR_VERSION_VALUE));
|
|
#endif
|
|
}
|
|
|
|
// Execute lib/node/init.ts.
|
|
v8::LocalVector<v8::String> bundle_params(
|
|
env->isolate(), {node::FIXED_ONE_BYTE_STRING(env->isolate(), "process"),
|
|
node::FIXED_ONE_BYTE_STRING(env->isolate(), "require")});
|
|
v8::LocalVector<v8::Value> bundle_args(env->isolate(), {process, require});
|
|
electron::util::CompileAndCall(env->isolate(), env->context(),
|
|
"electron/js2c/node_init", &bundle_params,
|
|
&bundle_args);
|
|
}
|
|
|
|
} // namespace electron
|