910e62b5创建于 1月15日历史提交
// Copyright 2012 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/test/chromedriver/session.h"

#include <algorithm>
#include <list>
#include <utility>

#include "base/containers/flat_map.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/values.h"
#include "chrome/test/chromedriver/chrome/chrome.h"
#include "chrome/test/chromedriver/chrome/devtools_client.h"
#include "chrome/test/chromedriver/chrome/status.h"
#include "chrome/test/chromedriver/chrome/web_view.h"
#include "chrome/test/chromedriver/logging.h"
#include "chrome/test/chromedriver/net/timeout.h"

namespace {

constinit thread_local Session* session = nullptr;

}  // namespace

namespace internal {

Status SplitChannel(std::string* channel,
                    int* connection_id,
                    std::string* suffix) {
  DCHECK(channel);  // precondition
  size_t k = channel->size();
  for (; k && (*channel)[k - 1] != '/'; --k) {
  }
  if (k == 0) {
    return Status{kUnknownError,
                  "goog:channel does not end with an expected suffix"};
  }
  *suffix = channel->substr(k - 1);
  channel->erase(std::next(channel->begin(), k - 1), channel->end());
  --k;

  for (; k && (*channel)[k - 1] != '/'; --k) {
  }
  if (k == 0) {
    return Status{kUnknownError, "goog:channel does not contain connection_id"};
  }
  std::string connection_str = channel->substr(k);
  channel->erase(std::next(channel->begin(), k - 1), channel->end());
  if (!base::StringToInt(connection_str, connection_id)) {
    return Status{kUnknownError,
                  "connection_id in the channel must be integer"};
  }

  return Status{kOk};
}
}  // namespace internal

FrameInfo::FrameInfo(const std::string& parent_frame_id,
                     const std::string& frame_id,
                     const std::string& chromedriver_frame_id)
    : parent_frame_id(parent_frame_id),
      frame_id(frame_id),
      chromedriver_frame_id(chromedriver_frame_id) {}

InputCancelListEntry::InputCancelListEntry(base::Value::Dict* input_state,
                                           const MouseEvent* mouse_event,
                                           const TouchEvent* touch_event,
                                           const KeyEvent* key_event)
    : input_state(input_state) {
  if (mouse_event != nullptr) {
    this->mouse_event = std::make_unique<MouseEvent>(*mouse_event);
    this->mouse_event->type = kReleasedMouseEventType;
  } else if (touch_event != nullptr) {
    this->touch_event = std::make_unique<TouchEvent>(*touch_event);
    this->touch_event->type = kTouchEnd;
  } else if (key_event != nullptr) {
    this->key_event = std::make_unique<KeyEvent>(*key_event);
    this->key_event->type = kKeyUpEventType;
  }
}

InputCancelListEntry::InputCancelListEntry(InputCancelListEntry&& other) =
    default;

InputCancelListEntry::~InputCancelListEntry() = default;

BidiConnection::BidiConnection(int connection_id,
                               SendTextFunc send_response,
                               CloseFunc close_connection)
    : connection_id(connection_id),
      send_response(std::move(send_response)),
      close_connection(std::move(close_connection)) {}

BidiConnection::BidiConnection(BidiConnection&& other) = default;

BidiConnection::~BidiConnection() = default;

BidiConnection& BidiConnection::operator=(BidiConnection&& other) = default;

// The default timeout values came from W3C spec.
const base::TimeDelta Session::kDefaultImplicitWaitTimeout = base::Seconds(0);
const base::TimeDelta Session::kDefaultPageLoadTimeout = base::Seconds(300);
const base::TimeDelta Session::kDefaultScriptTimeout = base::Seconds(30);
// The extra timeout values.
const base::TimeDelta Session::kDefaultBrowserStartupTimeout =
    base::Seconds(60);
const char Session::kChannelSuffix[] = "/chan";

Session::Session(const std::string& id)
    : id(id),
      w3c_compliant(kW3CDefault),
      quit(false),
      detach(false),
      sticky_modifiers(0),
      mouse_position(0, 0),
      pressed_mouse_button(kNoneMouseButton),
      implicit_wait(kDefaultImplicitWaitTimeout),
      page_load_timeout(kDefaultPageLoadTimeout),
      script_timeout(kDefaultScriptTimeout),
      strict_file_interactability(false),
      click_count(0),
      mouse_click_timestamp(base::TimeTicks::Now()) {}

Session::Session(const std::string& id, std::unique_ptr<Chrome> chrome)
    : Session(id) {
  this->chrome = std::move(chrome);
}

Session::Session(const std::string& id, const std::string& host) : Session(id) {
  this->host = host;
}

Session::~Session() = default;

Status Session::GetTargetWindow(WebView** web_view) {
  if (!chrome) {
    return Status(kNoSuchWindow, "no chrome started in this session");
  }

  WebView* tab = nullptr;
  Status status = chrome->GetWebViewById(window, &tab);
  if (status.IsError()) {
    return Status(kNoSuchWindow, "target window already closed", status);
  }

  if (tab->IsTab()) {
    Timeout timeout;
    status = tab->WaitForPendingActivePage(timeout);
    if (status.IsError()) {
      return status;
    }

    status = tab->GetActivePage(web_view);
  } else {
    // If target window is not a tab (eg. webview), return it as is.
    *web_view = tab;
  }
  return status;
}

void Session::SwitchToTopFrame() {
  frames.clear();
  SwitchFrameInternal(true);
}

void Session::SwitchToParentFrame() {
  if (!frames.empty())
    frames.pop_back();
  SwitchFrameInternal(false);
}

void Session::SwitchToSubFrame(const std::string& frame_id,
                               const std::string& chromedriver_frame_id) {
  std::string parent_frame_id;
  if (!frames.empty())
    parent_frame_id = frames.back().frame_id;
  frames.push_back(FrameInfo(parent_frame_id, frame_id, chromedriver_frame_id));
  SwitchFrameInternal(false);
}

std::string Session::GetCurrentFrameId() const {
  if (frames.empty())
    return std::string();
  return frames.back().frame_id;
}

std::vector<WebDriverLog*> Session::GetAllLogs() const {
  std::vector<WebDriverLog*> logs;
  for (const auto& log : devtools_logs)
    logs.push_back(log.get());
  if (driver_log)
    logs.push_back(driver_log.get());
  return logs;
}

void Session::SwitchFrameInternal(bool for_top_frame) {
  WebView* web_view = nullptr;
  Status status = GetTargetWindow(&web_view);
  if (!status.IsError()) {
    if (for_top_frame)
      web_view->SetFrame(std::string());
    else
      web_view->SetFrame(GetCurrentFrameId());
  } else {
    // Do nothing; this should be very rare because callers of this function
    // have already called GetTargetWindow.
    // Let later code handle issues that arise from the invalid state.
  }
}

Status Session::OnBidiResponse(base::Value::Dict payload) {
  std::string* channel = payload.FindString("goog:channel");
  if (!channel) {
    return Status{kUnknownError,
                  "goog:channel is missing in the BiDi response"};
  }

  int connection_id = -1;
  std::string suffix;
  Status status = internal::SplitChannel(channel, &connection_id, &suffix);
  if (status.IsError()) {
    return status;
  }

  if (channel->empty()) {
    payload.Remove("goog:channel");
  } else if (suffix != kChannelSuffix) {
    return Status{kUnknownError,
                  "unexpected channel name in the BiDi response"};
  }

  std::string message;
  // `OPTIONS_OMIT_DOUBLE_TYPE_PRESERVATION` is needed to keep the BiDi format.
  // crbug.com/chromedriver/4297.
  if (!base::JSONWriter::WriteWithOptions(
          payload, base::JSONWriter::OPTIONS_OMIT_DOUBLE_TYPE_PRESERVATION,
          &message)) {
    return Status{kUnknownError, "unable to serialize a BiDi response"};
  }

  auto it = std::ranges::find(bidi_connections_, connection_id,
                              &BidiConnection::connection_id);
  if (it == bidi_connections_.end()) {
    // It can happen that we receive a message from the mapper designated to the
    // channel that has recently been closed.
    LOG(INFO) << "BiDi connection is closed. Skipping the BiDiMapper message: "
              << message;
    return Status{kOk};
  }

  it->send_response.Run(std::move(message));
  return Status{kOk};
}

void Session::AddBidiConnection(int connection_id,
                                SendTextFunc send_response,
                                CloseFunc close_connection) {
  bidi_connections_.emplace_back(connection_id, std::move(send_response),
                                 std::move(close_connection));
}

void Session::RemoveBidiConnection(int connection_id) {
  // As connections can be closed by both remote and local ends
  // we don't treat an attempt to close a non-existing (presumably closed)
  // connection as an error.
  // Reallistically we will not have many connections, therefore linear search
  // is optimal.
  auto it = std::ranges::find(bidi_connections_, connection_id,
                              &BidiConnection::connection_id);
  if (it != bidi_connections_.end()) {
    bidi_connections_.erase(it);
  }
}

void Session::CloseAllConnections() {
  for (BidiConnection& conn : bidi_connections_) {
    // If the callback fails (asynchronously) because the connection was
    // terminated we simply ignore this - it is already closed.
    conn.close_connection.Run();
  }
  bidi_connections_.clear();
}

void Session::Terminate() {
  Session* s = session;
  if (s == nullptr) {
    return;
  }
  s->CloseAllConnections();
  SetThreadLocalSession(std::unique_ptr<Session>());
  if (s->terminate_on_cmd) {
    s->cmd_task_runner->PostTask(FROM_HERE, std::move(s->terminate_on_cmd));
  }
  delete s;
}

Status Session::SendBidiSessionEnd() {
  WebView* web_view = nullptr;
  Status status =
      chrome->GetActivePageByWebViewId(bidi_mapper_web_view_id, &web_view,
                                       /*wait_for_page=*/false);
  if (status.IsError()) {
    return status;
  }
  base::Value::Dict bidi_cmd;
  bidi_cmd.Set("goog:channel", "/before-session-shutdown");
  bidi_cmd.Set("id", 1);
  bidi_cmd.Set("method", "session.end");
  bidi_cmd.Set("params", base::Value::Dict());
  base::Value::Dict response;
  Timeout timeout(base::Seconds(20));
  return web_view->SendBidiCommand(std::move(bidi_cmd), timeout, response);
}

void Session::HandleMessagesAndTerminateIfNecessary() {
  if (!session || !session->web_socket_url) {
    return;
  }

  Status status = session->chrome->Client()->HandleReceivedEvents();

  if (session->chrome->GetWebViewCount() <= 1) {
    // There can be web views created by BiDi Mapper. Update ChromeDriver web
    // views.
    std::list<std::string> tab_view_ids;
    status = session->chrome->GetTopLevelWebViewIds(&tab_view_ids,
                                                    session->w3c_compliant);
    if (status.IsError()) {
      VLOG(0) << "error while updating top level web views: "
              << status.message();
    }
  }

  if (status.IsOk() && session->chrome->GetWebViewCount() > 1) {
    return;
  }

  // Either is true:
  // * status.IsError()
  // * web view count <= 1

  if (status.code() != kDisconnected) {
    VLOG(0) << "error while processing messages from the browser: "
            << status.message();
    if (session->chrome->GetWebViewCount() > 1) {
      return;
    }
  }

  // Either is true:
  // * the error is kDisconnected
  // * web view count <= 1
  // In both cases the session must be terminated.

  if (!status.IsError()) {
    // The web view count is <= 1
    status = session->SendBidiSessionEnd();
    if (status.IsError()) {
      VLOG(0) << "error while terminating a BiDi session: " << status.message();
    }
    status = Status{kInvalidSessionId};
  }

  base::flat_map<StatusCode, std::string> fatal_errors = {
      {kDisconnected, "session deleted due to browser connection loss"},
      {kInvalidSessionId, "session deleted as no more views are available"},
  };

  DCHECK(fatal_errors.contains(status.code()));

  session->quit = true;
  std::string message = fatal_errors[status.code()];
  // Even though the connection was lost that makes the graceful
  // shutdown impossible the Quit procedure falls back on killing the
  // process in case if it is still alive.
  if (!session->detach) {
    Status quit_status = session->chrome->Quit();
    if (quit_status.IsError()) {
      message += ", but failed to kill browser:" + quit_status.message();
    }
  }
  VLOG(0) << message;

  Terminate();
}

Session* GetThreadLocalSession() {
  return session;
}

void SetThreadLocalSession(std::unique_ptr<Session> new_session) {
  session = new_session.release();
}