blob: 9860507291b522ab613d235b61340d9dc600a7d8 [file] [log] [blame]
// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/test/chromedriver/chrome/chrome_desktop_impl.h"
#include <stddef.h>
#include <utility>
#include "base/files/file_path.h"
#include "base/logging.h"
#include "base/posix/eintr_wrapper.h"
#include "base/process/kill.h"
#include "base/strings/string_util.h"
#include "base/sys_info.h"
#include "base/threading/platform_thread.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/test/chromedriver/chrome/automation_extension.h"
#include "chrome/test/chromedriver/chrome/devtools_client.h"
#include "chrome/test/chromedriver/chrome/devtools_event_listener.h"
#include "chrome/test/chromedriver/chrome/devtools_http_client.h"
#include "chrome/test/chromedriver/chrome/status.h"
#include "chrome/test/chromedriver/chrome/web_view_impl.h"
#include "chrome/test/chromedriver/net/port_server.h"
#include "chrome/test/chromedriver/net/timeout.h"
#if defined(OS_POSIX)
#include <errno.h>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
#endif
namespace {
// Enables wifi and data only, not airplane mode.
const int kDefaultConnectionType = 6;
bool KillProcess(const base::Process& process, bool kill_gracefully) {
#if defined(OS_POSIX)
if (!kill_gracefully) {
kill(process.Pid(), SIGKILL);
base::TimeTicks deadline =
base::TimeTicks::Now() + base::TimeDelta::FromSeconds(30);
while (base::TimeTicks::Now() < deadline) {
pid_t pid = HANDLE_EINTR(waitpid(process.Pid(), NULL, WNOHANG));
if (pid == process.Pid())
return true;
if (pid == -1) {
if (errno == ECHILD) {
// The wait may fail with ECHILD if another process also waited for
// the same pid, causing the process state to get cleaned up.
return true;
}
LOG(WARNING) << "Error waiting for process " << process.Pid();
}
base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(50));
}
return false;
}
#endif
if (!process.Terminate(0, true)) {
int exit_code;
return base::GetTerminationStatus(process.Handle(), &exit_code) !=
base::TERMINATION_STATUS_STILL_RUNNING;
}
return true;
}
} // namespace
ChromeDesktopImpl::ChromeDesktopImpl(
std::unique_ptr<DevToolsHttpClient> http_client,
std::unique_ptr<DevToolsClient> websocket_client,
std::vector<std::unique_ptr<DevToolsEventListener>>
devtools_event_listeners,
std::unique_ptr<PortReservation> port_reservation,
std::string page_load_strategy,
base::Process process,
const base::CommandLine& command,
base::ScopedTempDir* user_data_dir,
base::ScopedTempDir* extension_dir,
bool network_emulation_enabled)
: ChromeImpl(std::move(http_client),
std::move(websocket_client),
std::move(devtools_event_listeners),
std::move(port_reservation),
page_load_strategy),
process_(std::move(process)),
command_(command),
network_connection_enabled_(network_emulation_enabled),
network_connection_(kDefaultConnectionType) {
if (user_data_dir->IsValid())
CHECK(user_data_dir_.Set(user_data_dir->Take()));
if (extension_dir->IsValid())
CHECK(extension_dir_.Set(extension_dir->Take()));
}
ChromeDesktopImpl::~ChromeDesktopImpl() {
if (!quit_) {
base::FilePath user_data_dir = user_data_dir_.Take();
base::FilePath extension_dir = extension_dir_.Take();
LOG(WARNING) << "chrome quit unexpectedly, leaving behind temporary "
"directories for debugging:";
if (user_data_dir_.IsValid())
LOG(WARNING) << "chrome user data directory: " << user_data_dir.value();
if (extension_dir_.IsValid())
LOG(WARNING) << "chromedriver automation extension directory: "
<< extension_dir.value();
}
}
Status ChromeDesktopImpl::WaitForPageToLoad(
const std::string& url,
const base::TimeDelta& timeout_raw,
std::unique_ptr<WebView>* web_view,
bool w3c_compliant) {
Timeout timeout(timeout_raw);
std::string id;
WebViewInfo::Type type = WebViewInfo::Type::kPage;
while (timeout.GetRemainingTime() > base::TimeDelta()) {
WebViewsInfo views_info;
Status status = devtools_http_client_->GetWebViewsInfo(&views_info);
if (status.IsError())
return status;
for (size_t i = 0; i < views_info.GetSize(); ++i) {
const WebViewInfo& view_info = views_info.Get(i);
if (base::StartsWith(view_info.url, url, base::CompareCase::SENSITIVE)) {
id = view_info.id;
type = view_info.type;
break;
}
}
if (!id.empty())
break;
base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(100));
}
if (id.empty())
return Status(kUnknownError, "page could not be found: " + url);
const DeviceMetrics* device_metrics = devtools_http_client_->device_metrics();
if (type == WebViewInfo::Type::kApp ||
type == WebViewInfo::Type::kBackgroundPage) {
// Apps and extensions don't work on Android, so it doesn't make sense to
// provide override device metrics in mobile emulation mode, and can also
// potentially crash the renderer, for more details see:
// https://code.google.com/p/chromedriver/issues/detail?id=1205
device_metrics = nullptr;
}
std::unique_ptr<WebView> web_view_tmp(
new WebViewImpl(id, w3c_compliant, devtools_http_client_->browser_info(),
devtools_http_client_->CreateClient(id), device_metrics,
page_load_strategy()));
Status status = web_view_tmp->ConnectIfNecessary();
if (status.IsError())
return status;
status = web_view_tmp->WaitForPendingNavigations(
std::string(), timeout, false);
if (status.IsOk())
*web_view = std::move(web_view_tmp);
return status;
}
Status ChromeDesktopImpl::GetAutomationExtension(
AutomationExtension** extension,
bool w3c_compliant) {
if (!automation_extension_) {
std::unique_ptr<WebView> web_view;
Status status = WaitForPageToLoad(
"chrome-extension://aapnijgdinlhnhlmodcfapnahmbfebeb/"
"_generated_background_page.html",
base::TimeDelta::FromSeconds(10),
&web_view,
w3c_compliant);
if (status.IsError())
return Status(kUnknownError, "cannot get automation extension", status);
automation_extension_.reset(new AutomationExtension(std::move(web_view)));
}
*extension = automation_extension_.get();
return Status(kOk);
}
Status ChromeDesktopImpl::GetAsDesktop(ChromeDesktopImpl** desktop) {
*desktop = this;
return Status(kOk);
}
std::string ChromeDesktopImpl::GetOperatingSystemName() {
return base::SysInfo::OperatingSystemName();
}
bool ChromeDesktopImpl::IsMobileEmulationEnabled() const {
return devtools_http_client_->device_metrics() != NULL;
}
bool ChromeDesktopImpl::HasTouchScreen() const {
return IsMobileEmulationEnabled();
}
bool ChromeDesktopImpl::IsNetworkConnectionEnabled() const {
return network_connection_enabled_;
}
Status ChromeDesktopImpl::QuitImpl() {
// If the Chrome session uses a custom user data directory, try sending a
// SIGTERM signal before SIGKILL, so that Chrome has a chance to write
// everything back out to the user data directory and exit cleanly. If we're
// using a temporary user data directory, we're going to delete the temporary
// directory anyway, so just send SIGKILL immediately.
bool kill_gracefully = !user_data_dir_.IsValid();
// If the Chrome session is being run with --log-net-log, send SIGTERM first
// to allow Chrome to write out all the net logs to the log path.
kill_gracefully |= command_.HasSwitch("log-net-log");
if (!KillProcess(process_, kill_gracefully))
return Status(kUnknownError, "cannot kill Chrome");
return Status(kOk);
}
const base::CommandLine& ChromeDesktopImpl::command() const {
return command_;
}
int ChromeDesktopImpl::GetNetworkConnection() const {
return network_connection_;
}
void ChromeDesktopImpl::SetNetworkConnection(
int network_connection) {
network_connection_ = network_connection;
}
Status ChromeDesktopImpl::GetWindowPosition(const std::string& target_id,
int* x,
int* y) {
Window window;
Status status = GetWindow(target_id, &window);
if (status.IsError())
return status;
*x = window.left;
*y = window.top;
return Status(kOk);
}
Status ChromeDesktopImpl::GetWindowSize(const std::string& target_id,
int* width,
int* height) {
Window window;
Status status = GetWindow(target_id, &window);
if (status.IsError())
return status;
*width = window.width;
*height = window.height;
return Status(kOk);
}
Status ChromeDesktopImpl::SetWindowRect(const std::string& target_id,
const base::DictionaryValue& params) {
Window window;
Status status = GetWindow(target_id, &window);
if (status.IsError())
return status;
auto bounds = std::make_unique<base::DictionaryValue>();
// fully exit fullscreen
if (window.state != "normal") {
auto bounds = std::make_unique<base::DictionaryValue>();
bounds->SetString("windowState", "normal");
status = SetWindowBounds(window.id, std::move(bounds));
if (status.IsError())
return status;
}
// window position
int x = 0;
int y = 0;
if (params.GetInteger("x", &x) && params.GetInteger("y", &y)) {
bounds->SetInteger("left", x);
bounds->SetInteger("top", y);
}
// window size
int width = 0;
int height = 0;
if (params.GetInteger("width", &width) &&
params.GetInteger("height", &height)) {
bounds->SetInteger("width", width);
bounds->SetInteger("height", height);
}
return SetWindowBounds(window.id, std::move(bounds));
}
Status ChromeDesktopImpl::SetWindowPosition(const std::string& target_id,
int x,
int y) {
Window window;
Status status = GetWindow(target_id, &window);
if (status.IsError())
return status;
if (window.state != "normal") {
// restore window to normal first to allow position change.
auto bounds = std::make_unique<base::DictionaryValue>();
bounds->SetString("windowState", "normal");
status = SetWindowBounds(window.id, std::move(bounds));
if (status.IsError())
return status;
}
auto bounds = std::make_unique<base::DictionaryValue>();
bounds->SetInteger("left", x);
bounds->SetInteger("top", y);
return SetWindowBounds(window.id, std::move(bounds));
}
Status ChromeDesktopImpl::SetWindowSize(const std::string& target_id,
int width,
int height) {
Window window;
Status status = GetWindow(target_id, &window);
if (status.IsError())
return status;
if (window.state != "normal") {
// restore window to normal first to allow size change.
auto bounds = std::make_unique<base::DictionaryValue>();
bounds->SetString("windowState", "normal");
status = SetWindowBounds(window.id, std::move(bounds));
if (status.IsError())
return status;
}
auto bounds = std::make_unique<base::DictionaryValue>();
bounds->SetInteger("width", width);
bounds->SetInteger("height", height);
return SetWindowBounds(window.id, std::move(bounds));
}
Status ChromeDesktopImpl::MaximizeWindow(const std::string& target_id) {
Window window;
Status status = GetWindow(target_id, &window);
if (status.IsError())
return status;
if (window.state == "maximized")
return Status(kOk);
if (window.state != "normal") {
// always restore window to normal first, since chrome ui doesn't allow
// maximizing a minimized or fullscreen window.
auto bounds = std::make_unique<base::DictionaryValue>();
bounds->SetString("windowState", "normal");
status = SetWindowBounds(window.id, std::move(bounds));
if (status.IsError())
return status;
}
auto bounds = std::make_unique<base::DictionaryValue>();
bounds->SetString("windowState", "maximized");
return SetWindowBounds(window.id, std::move(bounds));
}
Status ChromeDesktopImpl::MinimizeWindow(const std::string& target_id) {
Window window;
Status status = GetWindow(target_id, &window);
if (status.IsError())
return status;
if (window.state == "minimized")
return Status(kOk);
if (window.state != "normal") {
// restore window to normal first
auto bounds = std::make_unique<base::DictionaryValue>();
bounds->SetString("windowState", "normal");
status = SetWindowBounds(window.id, std::move(bounds));
if (status.IsError())
return status;
}
auto bounds = std::make_unique<base::DictionaryValue>();
bounds->SetString("windowState", "minimized");
return SetWindowBounds(window.id, std::move(bounds));
}
Status ChromeDesktopImpl::FullScreenWindow(const std::string& target_id) {
Window window;
Status status = GetWindow(target_id, &window);
if (status.IsError())
return status;
if (window.state == "fullscreen")
return Status(kOk);
if (window.state != "normal") {
auto bounds = std::make_unique<base::DictionaryValue>();
bounds->SetString("windowState", "normal");
status = SetWindowBounds(window.id, std::move(bounds));
if (status.IsError())
return status;
}
auto bounds = std::make_unique<base::DictionaryValue>();
bounds->SetString("windowState", "fullscreen");
return SetWindowBounds(window.id, std::move(bounds));
}
Status ChromeDesktopImpl::ParseWindowBounds(
std::unique_ptr<base::DictionaryValue> params,
Window* window) {
const base::Value* value = nullptr;
const base::DictionaryValue* bounds_dict = nullptr;
if (!params->Get("bounds", &value) || !value->GetAsDictionary(&bounds_dict))
return Status(kUnknownError, "no window bounds in response");
if (!bounds_dict->GetString("windowState", &window->state))
return Status(kUnknownError, "no window state in window bounds");
if (!bounds_dict->GetInteger("left", &window->left))
return Status(kUnknownError, "no left offset in window bounds");
if (!bounds_dict->GetInteger("top", &window->top))
return Status(kUnknownError, "no top offset in window bounds");
if (!bounds_dict->GetInteger("width", &window->width))
return Status(kUnknownError, "no width in window bounds");
if (!bounds_dict->GetInteger("height", &window->height))
return Status(kUnknownError, "no height in window bounds");
return Status(kOk);
}
Status ChromeDesktopImpl::ParseWindow(
std::unique_ptr<base::DictionaryValue> params,
Window* window) {
if (!params->GetInteger("windowId", &window->id))
return Status(kUnknownError, "no window id in response");
return ParseWindowBounds(std::move(params), window);
}
Status ChromeDesktopImpl::GetWindow(const std::string& target_id,
Window* window) {
Status status = devtools_websocket_client_->ConnectIfNecessary();
if (status.IsError())
return status;
base::DictionaryValue params;
params.SetString("targetId", target_id);
std::unique_ptr<base::DictionaryValue> result;
status = devtools_websocket_client_->SendCommandAndGetResult(
"Browser.getWindowForTarget", params, &result);
if (status.IsError())
return status;
return ParseWindow(std::move(result), window);
}
Status ChromeDesktopImpl::GetWindowBounds(int window_id, Window* window) {
Status status = devtools_websocket_client_->ConnectIfNecessary();
if (status.IsError())
return status;
base::DictionaryValue params;
params.SetInteger("windowId", window_id);
std::unique_ptr<base::DictionaryValue> result;
status = devtools_websocket_client_->SendCommandAndGetResult(
"Browser.getWindowBounds", params, &result);
if (status.IsError())
return status;
return ParseWindowBounds(std::move(result), window);
}
Status ChromeDesktopImpl::SetWindowBounds(
int window_id,
std::unique_ptr<base::DictionaryValue> bounds) {
Status status = devtools_websocket_client_->ConnectIfNecessary();
if (status.IsError())
return status;
base::DictionaryValue params;
params.SetInteger("windowId", window_id);
params.Set("bounds", bounds->CreateDeepCopy());
status = devtools_websocket_client_->SendCommand("Browser.setWindowBounds",
params);
if (status.IsError())
return status;
base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(100));
std::string state;
if (!bounds->GetString("windowState", &state))
return Status(kOk);
Window window;
status = GetWindowBounds(window_id, &window);
if (status.IsError())
return status;
if (window.state != state)
return Status(kUnknownError, "failed to change window state to " + state +
", current state is " + window.state);
return Status(kOk);
}