blob: ab7f6f73a88c7184c9883460b625b55fc2145931 [file] [log] [blame]
// 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();
}