| /* |
| * Copyright (C) 2009, 2012 Google Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are |
| * met: |
| * |
| * * Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * * Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following disclaimer |
| * in the documentation and/or other materials provided with the |
| * distribution. |
| * * Neither the name of Google Inc. nor the names of its |
| * contributors may be used to endorse or promote products derived from |
| * this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "third_party/blink/renderer/bindings/core/v8/worker_or_worklet_script_controller.h" |
| |
| #include <memory> |
| |
| #include "third_party/blink/public/platform/platform.h" |
| #include "third_party/blink/renderer/bindings/core/v8/script_controller.h" |
| #include "third_party/blink/renderer/core/execution_context/agent.h" |
| #include "third_party/blink/renderer/core/inspector/worker_thread_debugger.h" |
| #include "third_party/blink/renderer/core/workers/worker_global_scope.h" |
| #include "third_party/blink/renderer/core/workers/worker_or_worklet_global_scope.h" |
| #include "third_party/blink/renderer/platform/bindings/origin_trial_features.h" |
| #include "third_party/blink/renderer/platform/bindings/v8_dom_wrapper.h" |
| #include "third_party/blink/renderer/platform/bindings/wrapper_type_info.h" |
| #include "third_party/blink/renderer/platform/heap/heap.h" |
| #include "third_party/blink/renderer/platform/heap/thread_state.h" |
| #include "third_party/blink/renderer/platform/scheduler/public/event_loop.h" |
| #include "v8/include/v8.h" |
| |
| namespace blink { |
| |
| WorkerOrWorkletScriptController::WorkerOrWorkletScriptController( |
| WorkerOrWorkletGlobalScope* global_scope, |
| v8::Isolate* isolate) |
| : global_scope_(global_scope), |
| isolate_(isolate), |
| rejected_promises_(RejectedPromises::Create()) { |
| DCHECK(isolate); |
| world_ = |
| DOMWrapperWorld::Create(isolate, DOMWrapperWorld::WorldType::kWorker); |
| } |
| |
| WorkerOrWorkletScriptController::~WorkerOrWorkletScriptController() { |
| DCHECK(!rejected_promises_); |
| } |
| |
| void WorkerOrWorkletScriptController::Dispose() { |
| rejected_promises_->Dispose(); |
| rejected_promises_ = nullptr; |
| |
| DisposeContextIfNeeded(); |
| world_->Dispose(); |
| } |
| |
| void WorkerOrWorkletScriptController::DisposeContextIfNeeded() { |
| if (!IsContextInitialized()) |
| return; |
| |
| if (!global_scope_->IsMainThreadWorkletGlobalScope()) { |
| ScriptState::Scope scope(script_state_); |
| WorkerThreadDebugger* debugger = WorkerThreadDebugger::From(isolate_); |
| debugger->ContextWillBeDestroyed(global_scope_->GetThread(), |
| script_state_->GetContext()); |
| } |
| |
| { |
| ScriptState::Scope scope(script_state_); |
| v8::Local<v8::Context> context = script_state_->GetContext(); |
| // After disposing the world, all Blink->V8 references are gone. Blink |
| // stand-alone GCs may collect the WorkerOrWorkletGlobalScope because there |
| // are no more roots (V8->Blink references that are actually found by |
| // iterating Blink->V8 references). Clear the back pointers to avoid |
| // referring to cleared memory on the next GC in case the JS wrapper objects |
| // survived. |
| v8::Local<v8::Object> global_proxy_object = context->Global(); |
| v8::Local<v8::Object> global_object = |
| global_proxy_object->GetPrototype().As<v8::Object>(); |
| DCHECK(!global_object.IsEmpty()); |
| V8DOMWrapper::ClearNativeInfo(isolate_, global_object); |
| V8DOMWrapper::ClearNativeInfo(isolate_, global_proxy_object); |
| |
| // This detaches v8::MicrotaskQueue pointer from v8::Context, so that we can |
| // destroy EventLoop safely. |
| context->DetachGlobal(); |
| } |
| |
| script_state_->DisposePerContextData(); |
| script_state_->DissociateContext(); |
| } |
| |
| void WorkerOrWorkletScriptController::Initialize(const KURL& url_for_debugger) { |
| v8::HandleScope handle_scope(isolate_); |
| |
| DCHECK(!IsContextInitialized()); |
| |
| // Create a new v8::Context with the worker/worklet as the global object |
| // (aka the inner global). |
| auto* script_wrappable = static_cast<ScriptWrappable*>(global_scope_); |
| const WrapperTypeInfo* wrapper_type_info = |
| script_wrappable->GetWrapperTypeInfo(); |
| v8::Local<v8::FunctionTemplate> global_interface_template = |
| wrapper_type_info->GetV8ClassTemplate(isolate_, *world_) |
| .As<v8::FunctionTemplate>(); |
| DCHECK(!global_interface_template.IsEmpty()); |
| v8::Local<v8::ObjectTemplate> global_template = |
| global_interface_template->InstanceTemplate(); |
| v8::Local<v8::Context> context; |
| { |
| // Initialize V8 extensions before creating the context. |
| v8::ExtensionConfiguration extension_configuration = |
| ScriptController::ExtensionsFor(global_scope_); |
| |
| v8::MicrotaskQueue* microtask_queue = global_scope_->GetMicrotaskQueue(); |
| |
| V8PerIsolateData::UseCounterDisabledScope use_counter_disabled( |
| V8PerIsolateData::From(isolate_)); |
| context = v8::Context::New(isolate_, &extension_configuration, |
| global_template, v8::MaybeLocal<v8::Value>(), |
| v8::DeserializeInternalFieldsCallback(), |
| microtask_queue); |
| } |
| DCHECK(!context.IsEmpty()); |
| |
| script_state_ = |
| MakeGarbageCollected<ScriptState>(context, world_, global_scope_); |
| |
| ScriptState::Scope scope(script_state_); |
| |
| // Associate the global proxy object, the global object and the worker |
| // instance (C++ object) as follows. |
| // |
| // global proxy object <====> worker or worklet instance |
| // ^ |
| // | |
| // global object --------+ |
| // |
| // Per HTML spec, there is no corresponding object for workers to WindowProxy. |
| // However, V8 always creates the global proxy object, we associate these |
| // objects in the same manner as WindowProxy and Window. |
| // |
| // a) worker or worklet instance --> global proxy object |
| // As we shouldn't expose the global object to author scripts, we map the |
| // worker or worklet instance to the global proxy object. |
| // b) global proxy object --> worker or worklet instance |
| // Blink's callback functions are called by V8 with the global proxy object, |
| // we need to map the global proxy object to the worker or worklet instance. |
| // c) global object --> worker or worklet instance |
| // The global proxy object is NOT considered as a wrapper object of the |
| // worker or worklet instance because it's not an instance of |
| // v8::FunctionTemplate of worker or worklet, especially note that |
| // v8::Object::FindInstanceInPrototypeChain skips the global proxy object. |
| // Thus we need to map the global object to the worker or worklet instance. |
| |
| // The global proxy object. Note this is not the global object. |
| v8::Local<v8::Object> global_proxy = context->Global(); |
| v8::Local<v8::Object> associated_wrapper = |
| V8DOMWrapper::AssociateObjectWithWrapper(isolate_, script_wrappable, |
| wrapper_type_info, global_proxy); |
| CHECK(global_proxy == associated_wrapper); |
| |
| // The global object, aka worker/worklet wrapper object. |
| v8::Local<v8::Object> global_object = |
| global_proxy->GetPrototype().As<v8::Object>(); |
| V8DOMWrapper::SetNativeInfo(isolate_, global_object, wrapper_type_info, |
| script_wrappable); |
| |
| if (global_scope_->IsMainThreadWorkletGlobalScope()) { |
| // Set the human readable name for the world. |
| DCHECK(!global_scope_->Name().IsEmpty()); |
| world_->SetNonMainWorldHumanReadableName(world_->GetWorldId(), |
| global_scope_->Name()); |
| } else { |
| // Name new context for debugging. For main thread worklet global scopes |
| // this is done once the context is initialized. |
| WorkerThreadDebugger* debugger = WorkerThreadDebugger::From(isolate_); |
| debugger->ContextCreated(global_scope_->GetThread(), url_for_debugger, |
| context); |
| } |
| |
| if (!disable_eval_pending_.IsEmpty()) { |
| DisableEvalInternal(disable_eval_pending_); |
| disable_eval_pending_ = String(); |
| } |
| |
| // This is a workaround for worker with on-the-main-thread script fetch and |
| // worklets. |
| // - For workers with off-the-main-thread worker script fetch, |
| // PrepareForEvaluation() is called in WorkerGlobalScope::Initialize() after |
| // top-level worker script fetch and before script evaluation. |
| // - For workers with on-the-main-thread worker script fetch, it's too early |
| // to call PrepareForEvaluation() in WorkerGlobalScope::Initialize() because |
| // it's called immediately after WorkerGlobalScope's constructor, that is, |
| // before WorkerOrWorkletScriptController::Initialize(). Therefore, we |
| // ignore the first call of PrepareForEvaluation() from |
| // WorkerGlobalScope::Initialize(), and call it here again. |
| // TODO(https://crbug.com/835717): Remove this workaround once |
| // off-the-main-thread worker script fetch is enabled by default for dedicated |
| // workers. |
| // |
| // - For worklets, there is no appropriate timing to call |
| // PrepareForEvaluation() other than here because worklets have various |
| // initialization sequences depending on thread model (on-main-thread vs. |
| // off-main-thread) and unique script fetch (fetching a top-level script per |
| // addModule() call in JS). |
| // TODO(nhiroki): Unify worklet initialization sequences, and move this to an |
| // appropriate place. |
| if ((global_scope_->IsWorkerGlobalScope() && |
| To<WorkerGlobalScope>(global_scope_.Get()) |
| ->IsOffMainThreadScriptFetchDisabled()) || |
| global_scope_->IsWorkletGlobalScope()) { |
| // This should be called after origin trial tokens are applied for |
| // OriginTrialContext in WorkerGlobalScope::Initialize() to install origin |
| // trial features in JavaScript's global object. Workers with |
| // on-the-main-thread script fetch and worklets apply origin trial tokens |
| // before WorkerOrWorkletScriptController::initialize(), so it's safe to |
| // call this here. |
| PrepareForEvaluation(); |
| } |
| } |
| |
| void WorkerOrWorkletScriptController::PrepareForEvaluation() { |
| if (!IsContextInitialized()) { |
| // For workers with on-the-main-thread worker script fetch, this can be |
| // called before WorkerOrWorkletScriptController::Initialize() via |
| // WorkerGlobalScope creation function. In this case, PrepareForEvaluation() |
| // calls this function again. See comments in PrepareForEvaluation(). |
| DCHECK(global_scope_->IsWorkerGlobalScope()); |
| DCHECK(To<WorkerGlobalScope>(global_scope_.Get()) |
| ->IsOffMainThreadScriptFetchDisabled()); |
| return; |
| } |
| DCHECK(!is_ready_to_evaluate_); |
| is_ready_to_evaluate_ = true; |
| |
| v8::HandleScope handle_scope(isolate_); |
| |
| #if defined(USE_BLINK_V8_BINDING_NEW_IDL_INTERFACE) |
| V8PerContextData* per_context_data = script_state_->PerContextData(); |
| ignore_result(per_context_data->ConstructorForType( |
| global_scope_->GetWrapperTypeInfo())); |
| #else // USE_BLINK_V8_BINDING_NEW_IDL_INTERFACE |
| ScriptState::Scope scope(script_state_); |
| v8::Local<v8::Context> context = script_state_->GetContext(); |
| |
| auto* script_wrappable = static_cast<ScriptWrappable*>(global_scope_); |
| const WrapperTypeInfo* wrapper_type_info = |
| script_wrappable->GetWrapperTypeInfo(); |
| |
| // All interfaces must be registered to V8PerContextData. |
| // So we explicitly call constructorForType for the global object. |
| // This should be called after OriginTrialContext::AddTokens() in |
| // WorkerGlobalScope::Initialize() to install origin trial features. |
| V8PerContextData::From(context)->ConstructorForType(wrapper_type_info); |
| |
| v8::Local<v8::Object> global_object = |
| context->Global()->GetPrototype().As<v8::Object>(); |
| DCHECK(!global_object.IsEmpty()); |
| |
| v8::Local<v8::FunctionTemplate> global_interface_template = |
| wrapper_type_info->DomTemplate(isolate_, *world_); |
| DCHECK(!global_interface_template.IsEmpty()); |
| |
| wrapper_type_info->InstallConditionalFeatures( |
| context, *world_, global_object, v8::Local<v8::Object>(), |
| v8::Local<v8::Function>(), global_interface_template); |
| #endif // USE_BLINK_V8_BINDING_NEW_IDL_INTERFACE |
| } |
| |
| void WorkerOrWorkletScriptController::DisableEvalInternal( |
| const String& error_message) { |
| DCHECK(IsContextInitialized()); |
| DCHECK(!error_message.IsEmpty()); |
| |
| ScriptState::Scope scope(script_state_); |
| script_state_->GetContext()->AllowCodeGenerationFromStrings(false); |
| script_state_->GetContext()->SetErrorMessageForCodeGenerationFromStrings( |
| V8String(isolate_, error_message)); |
| } |
| |
| void WorkerOrWorkletScriptController::ForbidExecution() { |
| DCHECK(global_scope_->IsContextThread()); |
| execution_forbidden_ = true; |
| } |
| |
| bool WorkerOrWorkletScriptController::IsExecutionForbidden() const { |
| DCHECK(global_scope_->IsContextThread()); |
| return execution_forbidden_; |
| } |
| |
| void WorkerOrWorkletScriptController::DisableEval(const String& error_message) { |
| DCHECK(!error_message.IsEmpty()); |
| // Currently, this can be called before or after |
| // WorkerOrWorkletScriptController::Initialize() because of messy |
| // worker/worklet initialization sequences. Tidy them up after |
| // off-the-main-thread worker script fetch is enabled by default, make |
| // sure to call WorkerOrWorkletScriptController::DisableEval() after |
| // WorkerOrWorkletScriptController::Initialize(), and remove |
| // |disable_eval_pending_| logic (https://crbug.com/960770). |
| if (IsContextInitialized()) { |
| DisableEvalInternal(error_message); |
| return; |
| } |
| // `eval()` will actually be disabled on |
| // WorkerOrWorkletScriptController::Initialize() to be called from |
| // WorkerThread::InitializeOnWorkerThread() immediately and synchronously |
| // after returning here. Keep the error message until that time. |
| DCHECK(disable_eval_pending_.IsEmpty()); |
| disable_eval_pending_ = error_message; |
| } |
| |
| void WorkerOrWorkletScriptController::Trace(Visitor* visitor) const { |
| visitor->Trace(global_scope_); |
| visitor->Trace(script_state_); |
| } |
| |
| } // namespace blink |