blob: e299eb4fbf37cf97152e107fe1db39d2cddb0e50 [file] [log] [blame]
// Copyright 2021 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.
#ifndef CONTENT_SERVICES_AUCTION_WORKLET_AUCTION_V8_HELPER_H_
#define CONTENT_SERVICES_AUCTION_WORKLET_AUCTION_V8_HELPER_H_
#include <map>
#include <memory>
#include <string>
#include <vector>
#include "base/containers/span.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted.h"
#include "base/memory/ref_counted_delete_on_sequence.h"
#include "base/memory/scoped_refptr.h"
#include "base/sequence_checker.h"
#include "base/strings/string_piece.h"
#include "base/synchronization/lock.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "gin/public/isolate_holder.h"
#include "mojo/public/cpp/bindings/pending_associated_receiver.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/blink/public/mojom/devtools/devtools_agent.mojom.h"
#include "url/gurl.h"
#include "v8/include/v8-forward.h"
#include "v8/include/v8-isolate.h"
#include "v8/include/v8-persistent-handle.h"
namespace v8 {
class UnboundScript;
class WasmModuleObject;
} // namespace v8
namespace v8_inspector {
class V8Inspector;
} // namespace v8_inspector
namespace auction_worklet {
class AuctionV8DevToolsAgent;
class DebugCommandQueue;
// Helper for Javascript operations. Owns a V8 isolate, and manages operations
// on it. Must be deleted after all V8 objects created using its isolate. It
// facilitates creating objects from JSON and running scripts in isolated
// contexts.
//
// Currently, multiple AuctionV8Helpers can be in use at once, each will have
// its own V8 isolate. All AuctionV8Helpers are assumed to be created on the
// same thread (V8 startup is done only once per process, and not behind a
// lock). After creation, all public operations on the helper must be done on
// the thread represented by the `v8_runner` argument to Create(). It's the
// caller's responsibility to ensure that all other methods are used from the v8
// runner.
class AuctionV8Helper
: public base::RefCountedDeleteOnSequence<AuctionV8Helper> {
public:
// Timeout for script execution.
static const base::TimeDelta kScriptTimeout;
// Helper class to set up v8 scopes to use Isolate. All methods expect a
// FullIsolateScope to be have been created on the current thread, and a
// context to be entered.
class FullIsolateScope {
public:
explicit FullIsolateScope(AuctionV8Helper* v8_helper);
explicit FullIsolateScope(const FullIsolateScope&) = delete;
FullIsolateScope& operator=(const FullIsolateScope&) = delete;
~FullIsolateScope();
private:
const v8::Isolate::Scope isolate_scope_;
const v8::HandleScope handle_scope_;
};
// A wrapper for identifiers used to associate V8 context's with debugging
// primitives. Passed to methods like Compile and RunScript. If one is
// created, AbortDebuggerPauses() must be called before its destruction.
//
// This class is thread-safe, except SetResumeCallback must be used from V8
// thread.
class DebugId : public base::RefCountedThreadSafe<DebugId> {
public:
explicit DebugId(AuctionV8Helper* v8_helper);
// Returns V8 context group ID associated with this debug id.
int context_group_id() const { return context_group_id_; }
// Sets the callback to use to resume a worklet that's paused on startup.
// Must be called from the V8 thread.
//
// `resume_callback` will be invoked on the V8 thread; and should probably
// be bound to a a WeakPtr, since the invocation is ultimately via debugger
// mojo pipes, making its timing hard to relate to worklet lifetime.
void SetResumeCallback(base::OnceClosure resume_callback);
// If the JS thread is currently within AuctionV8Helper::RunScript() running
// code with this debug id, and the execution has been paused by the
// debugger, aborts the execution.
//
// Always prevents further debugger pauses of code associated with this
// debug id.
//
// This may be called from any thread, but note that posting this to the V8
// thread is unlikely to work, since this method is in particular useful for
// the cases where the V8 thread is blocked.
void AbortDebuggerPauses();
private:
friend class base::RefCountedThreadSafe<DebugId>;
~DebugId();
const scoped_refptr<AuctionV8Helper> v8_helper_;
const int context_group_id_;
};
explicit AuctionV8Helper(const AuctionV8Helper&) = delete;
AuctionV8Helper& operator=(const AuctionV8Helper&) = delete;
static scoped_refptr<AuctionV8Helper> Create(
scoped_refptr<base::SingleThreadTaskRunner> v8_runner);
static scoped_refptr<base::SingleThreadTaskRunner> CreateTaskRunner();
scoped_refptr<base::SequencedTaskRunner> v8_runner() const {
return v8_runner_;
}
v8::Isolate* isolate() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return isolate_holder_->isolate();
}
// Context that can be used for persistent items that can then be used in
// other contexts - compiling functions, creating objects, etc.
v8::Local<v8::Context> scratch_context() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return scratch_context_.Get(isolate());
}
// Create a v8::Context. The one thing this does that v8::Context::New() does
// not is remove access to the Date object.
v8::Local<v8::Context> CreateContext(
v8::Local<v8::ObjectTemplate> global_template =
v8::Local<v8::ObjectTemplate>());
// Creates a v8::String from an ASCII string literal, which should never fail.
v8::Local<v8::String> CreateStringFromLiteral(const char* ascii_string);
// Attempts to create a v8::String from a UTF-8 string. Returns empty string
// if input is not UTF-8.
v8::MaybeLocal<v8::String> CreateUtf8String(base::StringPiece utf8_string);
// The passed in JSON must be a valid UTF-8 JSON string.
v8::MaybeLocal<v8::Value> CreateValueFromJson(v8::Local<v8::Context> context,
base::StringPiece utf8_json);
// Convenience wrappers around the above Create* methods. Attempt to create
// the corresponding value type and append it to the passed in argument
// vector. Useful for assembling arguments to a Javascript function. Return
// false on failure.
[[nodiscard]] bool AppendUtf8StringValue(
base::StringPiece utf8_string,
std::vector<v8::Local<v8::Value>>* args);
[[nodiscard]] bool AppendJsonValue(v8::Local<v8::Context> context,
base::StringPiece utf8_json,
std::vector<v8::Local<v8::Value>>* args);
// Convenience wrapper that adds the specified value into the provided Object.
[[nodiscard]] bool InsertValue(base::StringPiece key,
v8::Local<v8::Value> value,
v8::Local<v8::Object> object);
// Convenience wrapper that creates an Object by parsing `utf8_json` as JSON
// and then inserts it into the provided Object.
[[nodiscard]] bool InsertJsonValue(v8::Local<v8::Context> context,
base::StringPiece key,
base::StringPiece utf8_json,
v8::Local<v8::Object> object);
// Attempts to convert |value| to JSON and write it to |out|. Returns false on
// failure.
bool ExtractJson(v8::Local<v8::Context> context,
v8::Local<v8::Value> value,
std::string* out);
// Compiles the provided script. Despite not being bound to a context, there
// still must be an active context for this method to be invoked. In case of
// an error sets `error_out`.
v8::MaybeLocal<v8::UnboundScript> Compile(
const std::string& src,
const GURL& src_url,
const DebugId* debug_id,
absl::optional<std::string>& error_out);
// Compiles the provided WASM module from bytecode. A context must be active
// for this method to be invoked, and the object would be created for it (but
// may be cloned efficiently for other contexts via CloneWasmModule). In case
// of an error sets `error_out`.
//
// Note that since the returned object is a JS Object, so to properly isolate
// different executions it should not be used directly but rather fresh copies
// should be made via CloneWasmModule.
v8::MaybeLocal<v8::WasmModuleObject> CompileWasm(
const std::string& payload,
const GURL& src_url,
const DebugId* debug_id,
absl::optional<std::string>& error_out);
// Creates a fresh object describing the same WASM module as `in`, which must
// not be empty. Can return an empty handle on an error.
//
// An execution context must be active, and the object will be created for it.
v8::MaybeLocal<v8::WasmModuleObject> CloneWasmModule(
v8::Local<v8::WasmModuleObject> in);
// Binds a script and runs it in the passed in context, returning the result.
// Note that the returned value could include references to objects or
// functions contained within the context, so is likely not safe to use in
// other contexts without sanitization.
//
// If `debug_id` is not nullptr, and a debugger connection has been
// instantiated, will notify debugger of `context`.
//
// Assumes passed in context is the active context. Passed in context must be
// using the Helper's isolate.
//
// Running this multiple times in the same context will re-load the entire
// script file in the context, and then run the script again.
//
// If `script_timeout` has no value, kScriptTimeout will be used as the
// default timeout.
//
// In case of an error sets `error_out`.
v8::MaybeLocal<v8::Value> RunScript(
v8::Local<v8::Context> context,
v8::Local<v8::UnboundScript> script,
const DebugId* debug_id,
base::StringPiece function_name,
base::span<v8::Local<v8::Value>> args,
absl::optional<base::TimeDelta> script_timeout,
std::vector<std::string>& error_out);
// If any debugging session targeting `debug_id` has set an active
// DOM instrumentation breakpoint `name`, asks for v8 to do a debugger pause
// on the next statement.
//
// Expected to be run before a corresponding RunScript.
void MaybeTriggerInstrumentationBreakpoint(const DebugId& debug_id,
const std::string& name);
void set_script_timeout_for_testing(base::TimeDelta script_timeout);
// Invokes the registered resume callback for given ID. Does nothing if it
// was already invoked.
void Resume(int context_group_id);
// Overrides what ID will be remembered as last returned to help check the
// allocation algorithm.
void SetLastContextGroupIdForTesting(int new_last_id);
// Calls Resume on all registered context group IDs.
void ResumeAllForTesting();
// Establishes a debugger connection, initializing debugging objects if
// needed, and associating the connection with the given `debug_id`.
//
// The debugger Mojo objects will primarily live on the v8 thread, but
// `mojo_sequence` will be used for a secondary communication channel in case
// the v8 thread is blocked. It must be distinct from v8_runner(). Only the
// value passed in for `mojo_sequence` the first time this method is called
// will be used.
void ConnectDevToolsAgent(
mojo::PendingAssociatedReceiver<blink::mojom::DevToolsAgent> agent,
scoped_refptr<base::SequencedTaskRunner> mojo_sequence,
const DebugId& debug_id);
// Returns the v8 inspector if one has been set. null if ConnectDevToolsAgent
// (or SetV8InspectorForTesting) hasn't been called.
v8_inspector::V8Inspector* inspector();
void SetV8InspectorForTesting(
std::unique_ptr<v8_inspector::V8Inspector> v8_inspector);
// Temporarily disables (and re-enables) script timeout for the currently
// running script. Total time elapsed when not paused will be kept track of.
//
// Must be called when within RunScript() only.
void PauseTimeoutTimer();
void ResumeTimeoutTimer();
// Returns the sequence where the timeout timer runs.
// This may be called on any thread.
scoped_refptr<base::SequencedTaskRunner> GetTimeoutTimerRunnerForTesting();
// Helper for formatting script name for debug messages.
std::string FormatScriptName(v8::Local<v8::UnboundScript> script);
private:
friend class base::RefCountedDeleteOnSequence<AuctionV8Helper>;
friend class base::DeleteHelper<AuctionV8Helper>;
class ScriptTimeoutHelper;
explicit AuctionV8Helper(
scoped_refptr<base::SingleThreadTaskRunner> v8_runner);
~AuctionV8Helper();
void CreateIsolate();
// These methods are used by DebugId, and except SetResumeCallback can be
// called from any thread.
int AllocContextGroupId();
void SetResumeCallback(int context_group_id,
base::OnceClosure resume_callback);
void AbortDebuggerPauses(int context_group_id);
void FreeContextGroupId(int context_group_id);
static std::string FormatExceptionMessage(v8::Local<v8::Context> context,
v8::Local<v8::Message> message);
static std::string FormatValue(v8::Isolate* isolate,
v8::Local<v8::Value> val);
scoped_refptr<base::SequencedTaskRunner> v8_runner_;
scoped_refptr<base::SequencedTaskRunner> timer_task_runner_;
std::unique_ptr<gin::IsolateHolder> isolate_holder_
GUARDED_BY_CONTEXT(sequence_checker_);
v8::Global<v8::Context> scratch_context_
GUARDED_BY_CONTEXT(sequence_checker_);
// Script timeout. Can be changed for testing.
base::TimeDelta script_timeout_ GUARDED_BY_CONTEXT(sequence_checker_) =
kScriptTimeout;
raw_ptr<ScriptTimeoutHelper> timeout_helper_
GUARDED_BY_CONTEXT(sequence_checker_) = nullptr;
base::Lock context_groups_lock_;
int last_context_group_id_ GUARDED_BY(context_groups_lock_) = 0;
// This is keyed by group IDs, and is used to keep track of what's valid.
std::map<int, base::OnceClosure> resume_callbacks_
GUARDED_BY(context_groups_lock_);
scoped_refptr<DebugCommandQueue> debug_command_queue_;
// Destruction order between `devtools_agent_` and `v8_inspector_` is
// relevant; see also comment in ~AuctionV8Helper().
std::unique_ptr<AuctionV8DevToolsAgent> devtools_agent_
GUARDED_BY_CONTEXT(sequence_checker_);
std::unique_ptr<v8_inspector::V8Inspector> v8_inspector_
GUARDED_BY_CONTEXT(sequence_checker_);
SEQUENCE_CHECKER(sequence_checker_);
};
} // namespace auction_worklet
#endif // CONTENT_SERVICES_AUCTION_WORKLET_AUCTION_V8_HELPER_H_