blob: b4867ca2a5dd69fb06922ab159aae078f48dc124 [file]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/renderer/actor/tool_executor.h"
#include <cstdint>
#include <memory>
#include "base/check.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/time/time.h"
#include "chrome/common/actor.mojom.h"
#include "chrome/common/actor/action_result.h"
#include "chrome/common/chrome_features.h"
#include "chrome/renderer/actor/click_tool.h"
#include "chrome/renderer/actor/drag_and_release_tool.h"
#include "chrome/renderer/actor/journal.h"
#include "chrome/renderer/actor/mouse_move_tool.h"
#include "chrome/renderer/actor/no_op_tool.h"
#include "chrome/renderer/actor/script_tool.h"
#include "chrome/renderer/actor/scroll_tool.h"
#include "chrome/renderer/actor/select_tool.h"
#include "chrome/renderer/actor/tool_utils.h"
#include "chrome/renderer/actor/type_tool.h"
#include "components/actor/core/journal_details_builder.h"
#include "components/actor/public/mojom/actor_types.mojom.h"
#include "content/public/renderer/render_frame.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "third_party/blink/public/web/web_node.h"
#include "ui/gfx/geometry/point_conversions.h"
using blink::WebLocalFrame;
using content::RenderFrame;
namespace actor {
ToolExecutor::ToolExecutor(RenderFrame* frame, Journal& journal)
: frame_(*frame), journal_(journal) {
CHECK(base::FeatureList::IsEnabled(features::kGlicActor));
}
ToolExecutor::~ToolExecutor() {
if (completion_callback_) {
std::move(completion_callback_)
.Run(MakeResult(mojom::ActionResultCode::kExecutorDestroyed,
/*requires_page_stabilization=*/false,
"The tool executor was destroyed before invocation "
"could complete."));
}
}
mojom::InitializeToolResultPtr ToolExecutor::InitializeTool(
mojom::ToolInvocationPtr invocation) {
is_split_execution_ = true;
return InitializeToolImpl(std::move(invocation));
}
mojom::InitializeToolResultPtr ToolExecutor::InitializeToolImpl(
mojom::ToolInvocationPtr invocation) {
auto init_entry = journal_->CreatePendingAsyncEntry(invocation->task_id,
"InitializeTool", {});
// Send the buffer now so the journal shows we received the message. This
// helps when debugging unresponsive renderers.
journal_->SendLogBuffer();
if (tool_) {
return mojom::InitializeToolResult::NewErrorResult(
MakeResult(mojom::ActionResultCode::kExecutorBusy));
}
CHECK_EQ(phase_, ExecutionPhase::kStart)
<< "InitializeTool called from invalid phase.";
WebLocalFrame* web_frame = frame_->GetWebFrame();
if (!web_frame || !web_frame->LocalRoot() ||
!web_frame->LocalRoot()->FrameWidget()) {
return mojom::InitializeToolResult::NewErrorResult(
MakeResult(mojom::ActionResultCode::kFrameWentAway));
}
switch (invocation->action->which()) {
case actor::mojom::ToolAction::Tag::kClick: {
tool_ = std::make_unique<ClickTool>(
frame_.get(), invocation->task_id, journal_.get(),
std::move(invocation->action->get_click()),
std::move(invocation->target),
std::move(invocation->observed_target));
break;
}
case actor::mojom::ToolAction::Tag::kMouseMove: {
tool_ = std::make_unique<MouseMoveTool>(
frame_.get(), invocation->task_id, journal_.get(),
std::move(invocation->action->get_mouse_move()),
std::move(invocation->target),
std::move(invocation->observed_target));
break;
}
case actor::mojom::ToolAction::Tag::kType: {
tool_ = std::make_unique<TypeTool>(
frame_.get(), invocation->task_id, journal_.get(),
std::move(invocation->action->get_type()),
std::move(invocation->target),
std::move(invocation->observed_target));
break;
}
case actor::mojom::ToolAction::Tag::kScroll: {
tool_ = std::make_unique<ScrollTool>(
frame_.get(), invocation->task_id, journal_.get(),
std::move(invocation->action->get_scroll()),
std::move(invocation->target),
std::move(invocation->observed_target));
break;
}
case actor::mojom::ToolAction::Tag::kSelect: {
tool_ = std::make_unique<SelectTool>(
frame_.get(), invocation->task_id, journal_.get(),
std::move(invocation->action->get_select()),
std::move(invocation->target),
std::move(invocation->observed_target));
break;
}
case actor::mojom::ToolAction::Tag::kDragAndRelease: {
tool_ = std::make_unique<DragAndReleaseTool>(
frame_.get(), invocation->task_id, journal_.get(),
std::move(invocation->action->get_drag_and_release()),
std::move(invocation->target),
std::move(invocation->observed_target));
break;
}
case actor::mojom::ToolAction::Tag::kScriptTool: {
// We could consider not waiting for stabilization since the API has an
// explicit async hook to know when the tool is done. Or having the
// stabilization only delay until a new frame is produced.
tool_ = std::make_unique<ScriptTool>(
frame_.get(), invocation->task_id, invocation->execution_id.value(),
journal_.get(), std::move(invocation->target),
std::move(invocation->observed_target),
std::move(invocation->action->get_script_tool()));
break;
}
case actor::mojom::ToolAction::Tag::kScrollTo: {
// This is only used to call `EnsureTargetInView()`.
tool_ = std::make_unique<NoOpTool>(
frame_.get(), invocation->task_id, journal_.get(),
std::move(invocation->target),
std::move(invocation->observed_target));
break;
}
default:
NOTREACHED();
}
// EnsureTargetInView() may synchronously dispatch DOM events (`beforematch`)
// that can run page script that detaches the owning frame and destroys
// this executor.
base::WeakPtr<ToolExecutor> weak_this = weak_ptr_factory_.GetWeakPtr();
const bool performed_scroll = tool_->EnsureTargetInView();
if (!weak_this) {
return mojom::InitializeToolResult::NewErrorResult(
MakeResult(mojom::ActionResultCode::kFrameWentAway));
}
if (performed_scroll) {
performed_scroll_into_view_ = true;
}
ValidationResult validation = tool_->Validate();
if (!IsOk(*validation.result)) {
// Add the error result with the current state before cleaning up.
validation.result->execution_end_time = base::TimeTicks::Now();
validation.result->requires_page_stabilization |=
performed_scroll_into_view_;
// Reset tool so that the ToolExecutor can receive new ToolInvocations if we
// are erroring after validation.
tool_.reset();
performed_scroll_into_view_ = false;
return mojom::InitializeToolResult::NewErrorResult(
std::move(validation.result));
}
std::optional<gfx::Point> point;
if (validation.target_point.has_value()) {
point = gfx::ToRoundedPoint(validation.target_point.value());
}
phase_ = ExecutionPhase::kInitialized;
return mojom::InitializeToolResult::NewSuccessPoint(point);
}
void ToolExecutor::ExecuteTool(const actor::TaskId& task_id,
ToolExecutorCallback callback) {
CHECK_EQ(phase_, ExecutionPhase::kInitialized)
<< "ExecuteTool called without successful InitializeTool.";
phase_ = ExecutionPhase::kExecuting;
execute_journal_entry_ = journal_->CreatePendingAsyncEntry(
task_id, "ExecuteTool",
JournalDetailsBuilder().Add("tool", tool_->DebugString()).Build());
// Send the buffer now so the journal shows we received the message. This
// helps when debugging unresponsive renderers.
journal_->SendLogBuffer();
CHECK(tool_);
CHECK_EQ(tool_->task_id(), task_id);
CHECK(!completion_callback_);
completion_callback_ = std::move(callback);
if (is_split_execution_) {
tool_->MarkAsRevalidation();
ValidationResult revalidation = tool_->Validate();
base::UmaHistogramSparse("Actor.Tools.RevalidationResult",
std::to_underlying(revalidation.result->code));
if (!IsOk(*revalidation.result)) {
ToolFinished(std::move(revalidation.result));
return;
}
}
tool_->Execute(base::BindOnce(&ToolExecutor::ToolFinished,
weak_ptr_factory_.GetWeakPtr()));
}
void ToolExecutor::InvokeTool(mojom::ToolInvocationPtr invocation,
ToolExecutorCallback callback) {
auto invoke_entry =
journal_->CreatePendingAsyncEntry(invocation->task_id, "InvokeTool", {});
// Send the buffer now so the journal shows we received the message. This
// helps when debugging unresponsive renderers.
journal_->SendLogBuffer();
actor::TaskId task_id = invocation->task_id;
// InitializeToolImpl() may synchronously dispatch DOM events which can
// destroy the owning frame and this executor.
base::WeakPtr<ToolExecutor> weak_this = weak_ptr_factory_.GetWeakPtr();
mojom::InitializeToolResultPtr result =
InitializeToolImpl(std::move(invocation));
if (!weak_this) {
return;
}
if (result->is_error_result()) {
// The tool failed to initialize because another tool is active. Abort this
// invocation immediately without disturbing the running tool.
if (result->get_error_result()->code ==
mojom::ActionResultCode::kExecutorBusy) {
std::move(callback).Run(std::move(result->get_error_result()));
return;
}
CHECK(!completion_callback_);
completion_callback_ = std::move(callback);
ToolFinished(std::move(result->get_error_result()));
return;
}
// Set after the busy check to avoid corrupting an active split-execution
// tool.
is_split_execution_ = false;
invoke_journal_entry_ = std::move(invoke_entry);
ExecuteTool(task_id, std::move(callback));
}
void ToolExecutor::CancelTool(const actor::TaskId& task_id) {
journal_->Log(
task_id, "ToolExecutor::CancelTool",
JournalDetailsBuilder().Add("tool_already_finished", !tool_).Build());
// Send the buffer now so the journal shows we received the message. This
// helps when debugging unresponsive renderers.
// TODO(b/467336183): This instance should be removed once this bug is
// resolved.
journal_->SendLogBuffer();
weak_ptr_factory_.InvalidateWeakPtrs();
if (!tool_) {
// Benign race condition: the tool has already finished.
CHECK(!completion_callback_);
// If the tool is already null, that means it has already finished and we
// should be at the start phase.
CHECK_EQ(phase_, ExecutionPhase::kStart);
return;
}
// The browser and renderer should agree on the active tool.
CHECK_EQ(tool_->task_id(), task_id);
// tool_->Cancel() synchronously dispatches DOM events that might destroy the
// owning frame and this executor. Use a weak pointer to detect if `this` is
// still valid.
base::WeakPtr<ToolExecutor> weak_this = weak_ptr_factory_.GetWeakPtr();
tool_->Cancel();
if (weak_this) {
// The result code doesn't matter as it will be ignored by the browser
// process.
ToolFinished(MakeResult(mojom::ActionResultCode::kInvokeCanceled));
}
}
void ToolExecutor::ToolFinished(mojom::ActionResultPtr result) {
phase_ = ExecutionPhase::kStart;
execute_journal_entry_.reset();
invoke_journal_entry_.reset();
result->execution_end_time = base::TimeTicks::Now();
result->requires_page_stabilization |= performed_scroll_into_view_;
// Reset for future ToolInvocations.
performed_scroll_into_view_ = false;
tool_.reset();
// The completion callback could be null if we receive a CancelTool call
// before ExecuteTool has started.
if (completion_callback_) {
std::move(completion_callback_).Run(std::move(result));
}
}
} // namespace actor