blob: 2bfcb3c67ab83c2f0635517e283af9fcf66cb0bd [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "modules/locks/LockManager.h"
#include <algorithm>
#include "bindings/core/v8/ScriptPromiseResolver.h"
#include "bindings/modules/v8/v8_lock_granted_callback.h"
#include "core/dom/AbortSignal.h"
#include "core/dom/DOMException.h"
#include "core/dom/ExceptionCode.h"
#include "core/frame/UseCounter.h"
#include "modules/locks/Lock.h"
#include "modules/locks/LockInfo.h"
#include "modules/locks/LockManagerSnapshot.h"
#include "mojo/public/cpp/bindings/binding.h"
#include "platform/bindings/Microtask.h"
#include "platform/bindings/ScriptState.h"
#include "platform/bindings/TraceWrapperMember.h"
#include "platform/heap/Persistent.h"
#include "platform/wtf/Functional.h"
#include "platform/wtf/Vector.h"
#include "services/service_manager/public/cpp/interface_provider.h"
namespace blink {
namespace {
constexpr char kRequestAbortedMessage[] = "The request was aborted.";
LockInfo ToLockInfo(const mojom::blink::LockInfoPtr& record) {
LockInfo info;
info.setMode(Lock::ModeToString(record->mode));
info.setName(record->name);
info.setClientId(record->client_id);
return info;
}
HeapVector<LockInfo> ToLockInfos(
const Vector<mojom::blink::LockInfoPtr>& records) {
HeapVector<LockInfo> out;
out.ReserveInitialCapacity(records.size());
for (const auto& record : records)
out.push_back(ToLockInfo(record));
return out;
}
} // namespace
class LockManager::LockRequestImpl final
: public GarbageCollectedFinalized<LockRequestImpl>,
public TraceWrapperBase,
public mojom::blink::LockRequest {
WTF_MAKE_NONCOPYABLE(LockRequestImpl);
EAGERLY_FINALIZE();
public:
LockRequestImpl(V8LockGrantedCallback* callback,
ScriptPromiseResolver* resolver,
const String& name,
mojom::blink::LockMode mode,
mojom::blink::LockRequestRequest request,
LockManager* manager)
: callback_(callback),
resolver_(resolver),
name_(name),
mode_(mode),
binding_(this, std::move(request)),
manager_(manager) {}
~LockRequestImpl() = default;
void Trace(blink::Visitor* visitor) {
visitor->Trace(resolver_);
visitor->Trace(manager_);
visitor->Trace(callback_);
}
// Wrapper tracing is needed for callbacks. The reference chain is
// NavigatorLocksImpl -> LockManager -> LockRequestImpl ->
// V8LockGrantedCallback.
void TraceWrappers(const ScriptWrappableVisitor* visitor) const override {
visitor->TraceWrappers(callback_);
}
// Called to immediately close the pipe which signals the back-end,
// unblocking further requests, without waiting for GC finalize the object.
void Cancel() { binding_.Close(); }
void Abort(const String& reason) override {
manager_->RemovePendingRequest(this);
binding_.Close();
if (!resolver_->GetScriptState()->ContextIsValid())
return;
resolver_->Reject(DOMException::Create(kAbortError, reason));
}
void Failed() override {
manager_->RemovePendingRequest(this);
binding_.Close();
ScriptState* script_state = resolver_->GetScriptState();
if (!script_state->ContextIsValid())
return;
// Lock was not granted e.g. because ifAvailable was specified but
// the lock was not available.
ScriptState::Scope scope(script_state);
v8::TryCatch try_catch(script_state->GetIsolate());
v8::Maybe<ScriptValue> result = callback_->Invoke(nullptr, nullptr);
if (try_catch.HasCaught()) {
resolver_->Reject(try_catch.Exception());
} else if (!result.IsNothing()) {
resolver_->Resolve(result.FromJust());
}
}
void Granted(mojom::blink::LockHandlePtr handle) override {
DCHECK(binding_.is_bound());
DCHECK(handle.is_bound());
manager_->RemovePendingRequest(this);
binding_.Close();
ScriptState* script_state = resolver_->GetScriptState();
if (!script_state->ContextIsValid()) {
// If a handle was returned, it will be automatically be released.
return;
}
Lock* lock =
Lock::Create(script_state, name_, mode_, std::move(handle), manager_);
manager_->held_locks_.insert(lock);
ScriptState::Scope scope(script_state);
v8::TryCatch try_catch(script_state->GetIsolate());
v8::Maybe<ScriptValue> result = callback_->Invoke(nullptr, lock);
if (try_catch.HasCaught()) {
lock->HoldUntil(
ScriptPromise::Reject(script_state, try_catch.Exception()),
resolver_);
} else if (!result.IsNothing()) {
lock->HoldUntil(ScriptPromise::Cast(script_state, result.FromJust()),
resolver_);
}
}
private:
// Callback passed by script; invoked when the lock is granted.
TraceWrapperMember<V8LockGrantedCallback> callback_;
// Rejects if the request was aborted, otherwise resolves/rejects with
// |callback_|'s result.
Member<ScriptPromiseResolver> resolver_;
// Held to stamp the Lock object's |name| property.
String name_;
// Held to stamp the Lock object's |mode| property.
mojom::blink::LockMode mode_;
mojo::Binding<mojom::blink::LockRequest> binding_;
// The |manager_| keeps |this| alive until a response comes in and this is
// registered. If the context is destroyed then |manager_| will dispose of
// |this| which terminates the request on the service side.
Member<LockManager> manager_;
};
LockManager::LockManager(ExecutionContext* context)
: ContextLifecycleObserver(context) {}
ScriptPromise LockManager::request(ScriptState* script_state,
const String& name,
V8LockGrantedCallback* callback,
ExceptionState& exception_state) {
return request(script_state, name, LockOptions(), callback, exception_state);
}
ScriptPromise LockManager::request(ScriptState* script_state,
const String& name,
const LockOptions& options,
V8LockGrantedCallback* callback,
ExceptionState& exception_state) {
ExecutionContext* context = ExecutionContext::From(script_state);
DCHECK(context->IsContextThread());
if (!context->GetSecurityOrigin()->CanAccessLocks()) {
exception_state.ThrowSecurityError(
"Access to the Locks API is denied in this context.");
return ScriptPromise();
}
if (context->GetSecurityOrigin()->IsLocal()) {
UseCounter::Count(context, WebFeature::kFileAccessedLocks);
}
if (!service_.get()) {
if (auto* provider = context->GetInterfaceProvider())
provider->GetInterface(mojo::MakeRequest(&service_));
if (!service_.get()) {
exception_state.ThrowTypeError("Service not available.");
return ScriptPromise();
}
}
mojom::blink::LockMode mode = Lock::StringToMode(options.mode());
if (options.steal() && options.ifAvailable()) {
exception_state.ThrowDOMException(
kNotSupportedError,
"The 'steal' and 'ifAvailable' options cannot be used together.");
return ScriptPromise();
}
if (name.StartsWith("-")) {
exception_state.ThrowDOMException(kNotSupportedError,
"Names cannot start with '-'.");
return ScriptPromise();
}
if (options.steal() && mode != mojom::blink::LockMode::EXCLUSIVE) {
exception_state.ThrowDOMException(
kNotSupportedError,
"The 'steal' option may only be used with 'exclusive' locks.");
return ScriptPromise();
}
if (options.hasSignal() && options.ifAvailable()) {
exception_state.ThrowDOMException(
kNotSupportedError,
"The 'signal' and 'ifAvailable' options cannot be used together.");
return ScriptPromise();
}
if (options.hasSignal() && options.steal()) {
exception_state.ThrowDOMException(
kNotSupportedError,
"The 'signal' and 'steal' options cannot be used together.");
return ScriptPromise();
}
if (options.hasSignal() && options.signal()->aborted()) {
exception_state.ThrowDOMException(kAbortError, kRequestAbortedMessage);
return ScriptPromise();
}
mojom::blink::LockManager::WaitMode wait =
options.steal()
? mojom::blink::LockManager::WaitMode::PREEMPT
: options.ifAvailable() ? mojom::blink::LockManager::WaitMode::NO_WAIT
: mojom::blink::LockManager::WaitMode::WAIT;
ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state);
ScriptPromise promise = resolver->Promise();
mojom::blink::LockRequestPtr request_ptr;
LockRequestImpl* request = new LockRequestImpl(
callback, resolver, name, mode, mojo::MakeRequest(&request_ptr), this);
AddPendingRequest(request);
if (options.hasSignal()) {
options.signal()->AddAlgorithm(WTF::Bind(&LockRequestImpl::Abort,
WrapWeakPersistent(request),
String(kRequestAbortedMessage)));
}
service_->RequestLock(name, mode, wait, std::move(request_ptr));
return promise;
}
ScriptPromise LockManager::query(ScriptState* script_state,
ExceptionState& exception_state) {
ExecutionContext* context = ExecutionContext::From(script_state);
DCHECK(context->IsContextThread());
if (!context->GetSecurityOrigin()->CanAccessLocks()) {
exception_state.ThrowSecurityError(
"Access to the Locks API is denied in this context.");
return ScriptPromise();
}
if (context->GetSecurityOrigin()->IsLocal()) {
UseCounter::Count(context, WebFeature::kFileAccessedLocks);
}
if (!service_.get()) {
if (auto* provider = context->GetInterfaceProvider())
provider->GetInterface(mojo::MakeRequest(&service_));
if (!service_.get()) {
exception_state.ThrowTypeError("Service not available.");
return ScriptPromise();
}
}
ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state);
ScriptPromise promise = resolver->Promise();
service_->QueryState(WTF::Bind(
[](ScriptPromiseResolver* resolver,
Vector<mojom::blink::LockInfoPtr> pending,
Vector<mojom::blink::LockInfoPtr> held) {
LockManagerSnapshot snapshot;
snapshot.setPending(ToLockInfos(pending));
snapshot.setHeld(ToLockInfos(held));
resolver->Resolve(snapshot);
},
WrapPersistent(resolver)));
return promise;
}
void LockManager::AddPendingRequest(LockRequestImpl* request) {
pending_requests_.insert(request);
}
void LockManager::RemovePendingRequest(LockRequestImpl* request) {
pending_requests_.erase(request);
}
void LockManager::Trace(blink::Visitor* visitor) {
ScriptWrappable::Trace(visitor);
ContextLifecycleObserver::Trace(visitor);
visitor->Trace(pending_requests_);
visitor->Trace(held_locks_);
}
void LockManager::TraceWrappers(const ScriptWrappableVisitor* visitor) const {
for (auto request : pending_requests_)
visitor->TraceWrappers(request);
ScriptWrappable::TraceWrappers(visitor);
}
void LockManager::ContextDestroyed(ExecutionContext*) {
for (auto request : pending_requests_)
request->Cancel();
pending_requests_.clear();
held_locks_.clear();
}
void LockManager::OnLockReleased(Lock* lock) {
// Lock may be removed by an explicit call and/or when the context is
// destroyed, so this must be idempotent.
held_locks_.erase(lock);
}
} // namespace blink