| // 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 |