chromium/services/accessibility/features/devtools/os_devtools_session.cc

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/351564777): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "services/accessibility/features/devtools/os_devtools_session.h"

#include <string_view>
#include <utility>

#include "base/functional/bind.h"
#include "base/notreached.h"
#include "base/strings/utf_string_conversions.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "services/accessibility/features/devtools/debug_command_queue.h"
#include "services/accessibility/features/devtools/os_devtools_agent.h"
#include "services/accessibility/features/v8_manager.h"
#include "third_party/inspector_protocol/crdtp/cbor.h"
#include "third_party/inspector_protocol/crdtp/json.h"
#include "v8/include/v8-microtask-queue.h"

namespace {
std::vector<uint8_t> GetStringBytes(const v8_inspector::StringView& s) {
  if (s.is8Bit()) {
    return std::vector<uint8_t>(s.characters8(), s.characters8() + s.length());
  }
  std::string converted = base::UTF16ToUTF8(std::u16string_view(
      reinterpret_cast<const char16_t*>(s.characters16()), s.length()));
  const uint8_t* data = reinterpret_cast<const uint8_t*>(converted.data());
  return std::vector<uint8_t>(data, data + converted.size());
}
}  // namespace

namespace ax {

// OSDevToolsSession::IOSession, which handles the pipe passed to the
// `io_session` parameter of DevToolsAgent::AttachDevToolsSession(), runs on a
// non-V8 sequence (except creation happens on the V8 thread). It's owned by the
// corresponding pipe.
//
// Its task is to forward messages to the v8 thread via DebugCommandQueue, with
// the `v8_thread_dispatch` callback being asked to run there to execute the
// command. The callback is responsible for dealing with possibility of the main
// session object being deleted.
class OSDevToolsSession::IOSession : public blink::mojom::DevToolsSession {
 public:
  using DispatchCallback =
      base::RepeatingCallback<void(int32_t call_id,
                                   const std::string& method,
                                   std::vector<uint8_t> message)>;

  ~IOSession() override {
    DCHECK_CALLED_ON_VALID_SEQUENCE(io_session_receiver_sequence_checker_);
  }

  static void Create(
      mojo::PendingReceiver<blink::mojom::DevToolsSession> io_session_receiver,
      scoped_refptr<base::SequencedTaskRunner> io_session_receiver_sequence,
      const scoped_refptr<DebugCommandQueue> debug_command_queue,
      DispatchCallback v8_thread_dispatch) {
    auto instance = base::WrapUnique(
        new IOSession(debug_command_queue, std::move(v8_thread_dispatch)));
    io_session_receiver_sequence->PostTask(
        FROM_HERE,
        base::BindOnce(&IOSession::ConnectReceiver, std::move(instance),
                       std::move(io_session_receiver)));
  }

  // DevToolsSession implementation.
  void DispatchProtocolCommand(int32_t call_id,
                               const std::string& method,
                               base::span<const uint8_t> message) override {
    DCHECK_CALLED_ON_VALID_SEQUENCE(io_session_receiver_sequence_checker_);
    debug_command_queue_->QueueTaskForV8Thread(
        base::BindOnce(v8_thread_dispatch_, call_id, method,
                       std::vector<uint8_t>(message.begin(), message.end())));
  }

 private:
  IOSession(const scoped_refptr<DebugCommandQueue> debug_command_queue,
            DispatchCallback v8_thread_dispatch)
      : debug_command_queue_(debug_command_queue),
        v8_thread_dispatch_(std::move(v8_thread_dispatch)) {
    DETACH_FROM_SEQUENCE(io_session_receiver_sequence_checker_);
  }

  static void ConnectReceiver(
      std::unique_ptr<IOSession> instance,
      mojo::PendingReceiver<blink::mojom::DevToolsSession>
          io_session_receiver) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(
        instance->io_session_receiver_sequence_checker_);
    mojo::MakeSelfOwnedReceiver(std::move(instance),
                                std::move(io_session_receiver));
  }

  const scoped_refptr<DebugCommandQueue> debug_command_queue_;
  DispatchCallback v8_thread_dispatch_;

  SEQUENCE_CHECKER(io_session_receiver_sequence_checker_);
};

OSDevToolsSession::OSDevToolsSession(
    V8Environment& v8_env,
    OSDevToolsAgent& agent,
    const scoped_refptr<DebugCommandQueue> debug_command_queue,
    const std::string& session_id,
    bool client_expects_binary_responses,
    bool session_waits_for_debugger,
    mojo::PendingAssociatedRemote<blink::mojom::DevToolsSessionHost> host,
    scoped_refptr<base::SequencedTaskRunner> io_session_receiver_sequence,
    mojo::PendingReceiver<blink::mojom::DevToolsSession> io_session_receiver,
    SessionDestroyedCallback on_delete_callback)
    : v8_env_(v8_env),
      debug_command_queue_(*debug_command_queue),
      session_id_(session_id),
      client_expects_binary_responses_(client_expects_binary_responses),
      on_delete_callback_(std::move(on_delete_callback)) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(v8_sequence_checker_);
  host_.Bind(std::move(host));
  // Connect v8 session.
  v8_session_ = agent.ConnectSession(this, session_waits_for_debugger);
  IOSession::Create(
      std::move(io_session_receiver), io_session_receiver_sequence,
      debug_command_queue,
      base::BindRepeating(&OSDevToolsSession::DispatchProtocolCommandFromIO,
                          weak_ptr_factory_.GetWeakPtr()));
}

OSDevToolsSession::~OSDevToolsSession() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(v8_sequence_checker_);
  std::move(on_delete_callback_).Run(this);
  v8_session_->stop();
}

base::OnceClosure OSDevToolsSession::MakeAbortPauseCallback() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(v8_sequence_checker_);
  // Note that this can be cancelled by the weak pointer only if the session
  // got unpaused by other means, since if it's paused it's not returning
  // control to the event loop, so Mojo won't get a chance to delete `this`.
  return base::BindOnce(&OSDevToolsSession::AbortDebuggerPause,
                        weak_ptr_factory_.GetWeakPtr());
}

void OSDevToolsSession::MaybeTriggerInstrumentationBreakpoint(
    const std::string& name) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(v8_sequence_checker_);
  // TODO(290815208)
}

void OSDevToolsSession::DispatchProtocolCommandFromIO(
    int32_t call_id,
    const std::string& method,
    std::vector<uint8_t> message) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(v8_sequence_checker_);
  DispatchProtocolCommand(call_id, method, message);
}

void OSDevToolsSession::DispatchProtocolCommand(
    int32_t call_id,
    const std::string& method,
    base::span<const uint8_t> message) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(v8_sequence_checker_);

  // Binary is always used to talk to V8 so it also only talks binary back,
  // making it easier to append session ID. That's also useful for
  // crdtp::Dispatchable.
  crdtp::span<uint8_t> message_span(message.data(), message.size());
  v8_inspector::StringView cbor_message;
  std::vector<uint8_t> converted_cbor_out;
  if (crdtp::cbor::IsCBORMessage(message_span)) {
    cbor_message = v8_inspector::StringView(message.data(), message.size());
  } else {
    crdtp::Status status =
        crdtp::json::ConvertJSONToCBOR(message_span, &converted_cbor_out);
    CHECK(status.ok()) << status.ToASCIIString();
    cbor_message = v8_inspector::StringView(converted_cbor_out.data(),
                                            converted_cbor_out.size());
  }

  if (v8_inspector::V8InspectorSession::canDispatchMethod(
          v8_inspector::StringView(
              reinterpret_cast<const uint8_t*>(method.data()),
              method.size()))) {
    // Need v8 isolate access.
    auto* isolate = v8_env_->GetIsolate();
    v8::Isolate::Scope isolate_scope(isolate);
    v8::HandleScope handle_scope(isolate);
    v8_session_->dispatchProtocolMessage(cbor_message);
    // Run microtasks.
    v8_env_->GetContext()->GetMicrotaskQueue()->PerformCheckpoint(isolate);
  } else {
    crdtp::Dispatchable dispatchable(crdtp::span<uint8_t>(
        cbor_message.characters8(), cbor_message.length()));
    fallback_dispatcher_.Dispatch(dispatchable).Run();
  }
}

void OSDevToolsSession::sendResponse(
    int call_id,
    std::unique_ptr<v8_inspector::StringBuffer> message) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(v8_sequence_checker_);
  SendProtocolResponseImpl(call_id, ::GetStringBytes(message.get()->string()));
}

void OSDevToolsSession::sendNotification(
    std::unique_ptr<v8_inspector::StringBuffer> message) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(v8_sequence_checker_);
  SendNotificationImpl(::GetStringBytes(message.get()->string()));
}

void OSDevToolsSession::flushProtocolNotifications() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(v8_sequence_checker_);
  NOTIMPLEMENTED();
}

void OSDevToolsSession::SendProtocolResponse(
    int call_id,
    std::unique_ptr<crdtp::Serializable> message) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(v8_sequence_checker_);
  SendProtocolResponseImpl(call_id, message->Serialize());
}

void OSDevToolsSession::SendProtocolNotification(
    std::unique_ptr<crdtp::Serializable> message) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(v8_sequence_checker_);
  SendNotificationImpl(message->Serialize());
}

void OSDevToolsSession::FallThrough(int call_id,
                                    crdtp::span<uint8_t> method,
                                    crdtp::span<uint8_t> message) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(v8_sequence_checker_);
  NOTIMPLEMENTED();
}

void OSDevToolsSession::FlushProtocolNotifications() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(v8_sequence_checker_);
  NOTIMPLEMENTED();
}

void OSDevToolsSession::AbortDebuggerPause() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(v8_sequence_checker_);
  // Note that if the session got resumed by other means before execution got
  // here V8 will simply ignore this call.
  v8_session_->resume(/*setTerminateOnResume=*/true);
}

void OSDevToolsSession::SendProtocolResponseImpl(int call_id,
                                                 std::vector<uint8_t> message) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(v8_sequence_checker_);
  crdtp::span<uint8_t> msg_span(message.data(), message.size());
  host_->DispatchProtocolResponse(FinalizeMessage(std::move(message)), call_id,
                                  nullptr);
}

void OSDevToolsSession::SendNotificationImpl(std::vector<uint8_t> message) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(v8_sequence_checker_);
  crdtp::span<uint8_t> msg_span(message.data(), message.size());
  host_->DispatchProtocolNotification(FinalizeMessage(std::move(message)),
                                      nullptr);
}

// Add session ID and maybe convert the message into JSON format, as
// documented as requirements in DevToolsAgent.AttachDevToolsSession mojo
// method documentation, and then encapsulate it inside a mojo
// DevToolsMessage.
//
// This is pretty much a copy-paste job from
// third_party/blink/renderer/core/inspector/devtools_session.cc.
// The primary difference is that namespacing of DevToolsMessage.
blink::mojom::DevToolsMessagePtr OSDevToolsSession::FinalizeMessage(
    std::vector<uint8_t> message) const {
  std::vector<uint8_t> message_to_send = std::move(message);
  if (!session_id_.empty()) {
    crdtp::Status status = crdtp::cbor::AppendString8EntryToCBORMap(
        crdtp::SpanFrom("sessionId"), crdtp::SpanFrom(session_id_),
        &message_to_send);
    CHECK(status.ok()) << status.ToASCIIString();
  }
  if (!client_expects_binary_responses_) {
    std::vector<uint8_t> json;
    crdtp::Status status =
        crdtp::json::ConvertCBORToJSON(crdtp::SpanFrom(message_to_send), &json);
    CHECK(status.ok()) << status.ToASCIIString();
    message_to_send = std::move(json);
  }
  auto mojo_msg = blink::mojom::DevToolsMessage::New();
  mojo_msg->data = std::move(message_to_send);
  return mojo_msg;
}

}  // namespace ax