blob: 048f67059ed5947222d9724d07f36cfa6335837e [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_launcher.h"
#include <stddef.h>
#include <stdint.h>
#include <algorithm>
#include <memory>
#include <utility>
#include <vector>
#include "base/base64.h"
#include "base/bind.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_file.h"
#include "base/format_macros.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/process/kill.h"
#include "base/process/launch.h"
#include "base/process/process.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/platform_thread.h"
#include "base/time/time.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/common/chrome_constants.h"
#include "chrome/common/chrome_result_codes.h"
#include "chrome/test/chromedriver/chrome/chrome_android_impl.h"
#include "chrome/test/chromedriver/chrome/chrome_desktop_impl.h"
#include "chrome/test/chromedriver/chrome/chrome_finder.h"
#include "chrome/test/chromedriver/chrome/chrome_remote_impl.h"
#include "chrome/test/chromedriver/chrome/device_manager.h"
#include "chrome/test/chromedriver/chrome/devtools_client_impl.h"
#include "chrome/test/chromedriver/chrome/devtools_event_listener.h"
#include "chrome/test/chromedriver/chrome/devtools_http_client.h"
#include "chrome/test/chromedriver/chrome/embedded_automation_extension.h"
#include "chrome/test/chromedriver/chrome/status.h"
#include "chrome/test/chromedriver/chrome/user_data_dir.h"
#include "chrome/test/chromedriver/chrome/version.h"
#include "chrome/test/chromedriver/chrome/web_view.h"
#include "chrome/test/chromedriver/constants/version.h"
#include "chrome/test/chromedriver/log_replay/chrome_replay_impl.h"
#include "chrome/test/chromedriver/log_replay/replay_http_client.h"
#include "chrome/test/chromedriver/net/net_util.h"
#include "components/crx_file/crx_verifier.h"
#include "crypto/rsa_private_key.h"
#include "crypto/sha2.h"
#include "services/network/public/mojom/url_loader_factory.mojom.h"
#include "third_party/zlib/google/zip.h"
#include "url/gurl.h"
#if defined(OS_POSIX)
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#elif defined(OS_WIN)
#include "chrome/test/chromedriver/keycode_text_conversion.h"
#endif
namespace {
const char* const kCommonSwitches[] = {
"disable-popup-blocking",
"enable-automation",
};
const char* const kDesktopSwitches[] = {
"disable-hang-monitor",
"disable-prompt-on-repost",
"disable-sync",
"no-first-run",
"disable-background-networking",
"disable-client-side-phishing-detection",
"disable-default-apps",
"enable-logging",
"log-level=0",
"password-store=basic",
"use-mock-keychain",
"test-type=webdriver",
// TODO(yoichio): This is temporary switch to support chrome internal
// components migration from the old web APIs.
// After completion of the migration, we should remove this.
// See crbug.com/911943 for detail.
"enable-blink-features=ShadowDOMV0",
};
const char* const kAndroidSwitches[] = {
"disable-fre", "enable-remote-debugging",
};
#if defined(OS_LINUX)
const char kEnableCrashReport[] = "enable-crash-reporter-for-testing";
#endif
const base::FilePath::CharType kDevToolsActivePort[] =
FILE_PATH_LITERAL("DevToolsActivePort");
Status UnpackAutomationExtension(const base::FilePath& temp_dir,
base::FilePath* automation_extension) {
std::string decoded_extension;
if (!base::Base64Decode(kAutomationExtension, &decoded_extension))
return Status(kUnknownError, "failed to base64decode automation extension");
base::FilePath extension_zip = temp_dir.AppendASCII("internal.zip");
int size = static_cast<int>(decoded_extension.length());
if (base::WriteFile(extension_zip, decoded_extension.c_str(), size)
!= size) {
return Status(kUnknownError, "failed to write automation extension zip");
}
base::FilePath extension_dir = temp_dir.AppendASCII("internal");
if (!zip::Unzip(extension_zip, extension_dir))
return Status(kUnknownError, "failed to unzip automation extension");
*automation_extension = extension_dir;
return Status(kOk);
}
Status PrepareDesktopCommandLine(const Capabilities& capabilities,
base::CommandLine* prepared_command,
base::ScopedTempDir* user_data_dir_temp_dir,
base::ScopedTempDir* extension_dir,
std::vector<std::string>* extension_bg_pages,
base::FilePath* user_data_dir) {
base::FilePath program = capabilities.binary;
if (program.empty()) {
if (!FindChrome(&program))
return Status(kUnknownError, base::StringPrintf("cannot find %s binary",
kBrowserShortName));
} else if (!base::PathExists(program)) {
return Status(
kUnknownError,
base::StringPrintf("no %s binary at %" PRFilePath,
base::ToLowerASCII(kBrowserShortName).c_str(),
program.value().c_str()));
}
base::CommandLine command(program);
Switches switches;
for (auto* common_switch : kCommonSwitches)
switches.SetUnparsedSwitch(common_switch);
for (auto* desktop_switch : kDesktopSwitches)
switches.SetUnparsedSwitch(desktop_switch);
for (const auto& excluded_switch : capabilities.exclude_switches) {
switches.RemoveSwitch(excluded_switch);
}
switches.SetFromSwitches(capabilities.switches);
if (!switches.HasSwitch("remote-debugging-port")) {
switches.SetSwitch("remote-debugging-port", "0");
}
if (capabilities.exclude_switches.count("user-data-dir") > 0) {
LOG(WARNING) << "excluding user-data-dir switch is not supported";
}
if (capabilities.exclude_switches.count("remote-debugging-port") > 0) {
LOG(WARNING) << "excluding remote-debugging-port switch is not supported";
}
if (switches.HasSwitch("user-data-dir")) {
base::FilePath::StringType userDataDir =
switches.GetSwitchValueNative("user-data-dir");
if (userDataDir.empty())
return Status(kInvalidArgument, "user data dir can not be empty");
*user_data_dir = base::FilePath(userDataDir);
} else {
command.AppendArg("data:,");
if (!user_data_dir_temp_dir->CreateUniqueTempDir())
return Status(kUnknownError, "cannot create temp dir for user data dir");
switches.SetSwitch("user-data-dir",
user_data_dir_temp_dir->GetPath().value());
*user_data_dir = user_data_dir_temp_dir->GetPath();
}
Status status = internal::PrepareUserDataDir(
*user_data_dir, capabilities.prefs.get(), capabilities.local_state.get());
if (status.IsError())
return status;
if (capabilities.exclude_switches.count("load-extension") > 0) {
if (capabilities.extensions.size() > 0)
return Status(
kUnknownError,
"cannot exclude load-extension switch when extensions are specified");
} else {
if (!extension_dir->CreateUniqueTempDir()) {
return Status(kUnknownError,
"cannot create temp dir for unpacking extensions");
}
status = internal::ProcessExtensions(
capabilities.extensions, extension_dir->GetPath(),
capabilities.use_automation_extension, &switches, extension_bg_pages);
if (status.IsError())
return status;
}
switches.AppendToCommandLine(&command);
*prepared_command = command;
return Status(kOk);
}
Status WaitForDevToolsAndCheckVersion(
const NetAddress& address,
network::mojom::URLLoaderFactory* factory,
const SyncWebSocketFactory& socket_factory,
const Capabilities* capabilities,
int wait_time,
std::unique_ptr<DevToolsHttpClient>* user_client,
bool* retry) {
std::unique_ptr<DeviceMetrics> device_metrics;
if (capabilities && capabilities->device_metrics)
device_metrics.reset(new DeviceMetrics(*capabilities->device_metrics));
std::unique_ptr<std::set<WebViewInfo::Type>> window_types;
if (capabilities && !capabilities->window_types.empty()) {
window_types.reset(
new std::set<WebViewInfo::Type>(capabilities->window_types));
} else {
window_types.reset(new std::set<WebViewInfo::Type>());
}
std::unique_ptr<DevToolsHttpClient> client;
base::CommandLine* cmd_line = base::CommandLine::ForCurrentProcess();
if (cmd_line->HasSwitch("devtools-replay")) {
base::CommandLine::StringType log_path =
cmd_line->GetSwitchValueNative("devtools-replay");
base::FilePath log_file_path(log_path);
client.reset(
new ReplayHttpClient(address, factory, socket_factory,
std::move(device_metrics), std::move(window_types),
capabilities->page_load_strategy, log_file_path));
} else {
client.reset(new DevToolsHttpClient(
address, factory, socket_factory, std::move(device_metrics),
std::move(window_types), capabilities->page_load_strategy));
}
const base::TimeTicks initial = base::TimeTicks::Now();
const base::TimeTicks deadline =
initial + base::TimeDelta::FromSeconds(wait_time);
Status status = client->Init(deadline - initial);
if (status.IsError())
return status;
const BrowserInfo* browser_info = client->browser_info();
if (browser_info->is_android &&
browser_info->android_package != capabilities->android_package) {
return Status(
kSessionNotCreated,
base::StringPrintf("please close '%s' and try again",
browser_info->android_package.c_str()));
}
*retry = true;
if (cmd_line->HasSwitch("disable-build-check")) {
LOG(WARNING) << "You are using an unsupported command-line switch: "
"--disable-build-check. Please don't report bugs that "
"cannot be reproduced with this switch removed.";
} else if (browser_info->major_version != kSupportedBrowserMajorVersion) {
if (browser_info->major_version == 0) {
// TODO(https://crbug.com/932013): Content Shell doesn't report a version
// number. Skip version checking with a warning.
LOG(WARNING) << "Unable to retrieve " << kBrowserShortName
<< " version. Unable to verify browser compatibility.";
} else if (browser_info->major_version ==
kSupportedBrowserMajorVersion + 1) {
// TODO(https://crbug.com/chromedriver/2656): Since we don't currently
// release ChromeDriver for dev or canary channels, allow using
// ChromeDriver version n (e.g., Beta) with Chrome version n+1 (e.g., Dev
// or Canary), with a warning.
LOG(WARNING) << "This version of " << kChromeDriverProductFullName
<< " has not been tested with " << kBrowserShortName
<< " version " << browser_info->major_version << ".";
} else {
*retry = false;
return Status(
kSessionNotCreated,
base::StringPrintf("This version of %s only supports %s version %d",
kChromeDriverProductFullName, kBrowserShortName,
kSupportedBrowserMajorVersion));
}
}
// Always try GetWebViewsInfo at least once if the client
// initialized successfully.
do {
WebViewsInfo views_info;
status = client->GetWebViewsInfo(&views_info);
if (status.IsError())
return status;
for (size_t i = 0; i < views_info.GetSize(); ++i) {
if (views_info.Get(i).type == WebViewInfo::kPage) {
*user_client = std::move(client);
return Status(kOk);
}
}
base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(50));
} while (base::TimeTicks::Now() < deadline);
return Status(kUnknownError, "unable to discover open pages");
}
Status CreateBrowserwideDevToolsClientAndConnect(
const NetAddress& address,
const PerfLoggingPrefs& perf_logging_prefs,
const SyncWebSocketFactory& socket_factory,
const std::vector<std::unique_ptr<DevToolsEventListener>>&
devtools_event_listeners,
const std::string& web_socket_url,
std::unique_ptr<DevToolsClient>* browser_client) {
std::string url(web_socket_url);
if (url.length() == 0) {
url = base::StringPrintf("ws://%s/devtools/browser/",
address.ToString().c_str());
}
std::unique_ptr<DevToolsClient> client(new DevToolsClientImpl(
socket_factory, url, DevToolsClientImpl::kBrowserwideDevToolsClientId));
for (const auto& listener : devtools_event_listeners) {
// Only add listeners that subscribe to the browser-wide |DevToolsClient|.
// Otherwise, listeners will think this client is associated with a webview,
// and will send unrecognized commands to it.
if (listener->subscribes_to_browser())
client->AddListener(listener.get());
}
// Provide the client regardless of whether it connects, so that Chrome always
// has a valid |devtools_websocket_client_|. If not connected, no listeners
// will be notified, and client will just return kDisconnected errors if used.
*browser_client = std::move(client);
// To avoid unnecessary overhead, only connect if tracing is enabled, since
// the browser-wide client is currently only used for tracing.
if (!perf_logging_prefs.trace_categories.empty()) {
Status status = (*browser_client)->ConnectIfNecessary();
if (status.IsError())
return status;
}
return Status(kOk);
}
Status LaunchRemoteChromeSession(
network::mojom::URLLoaderFactory* factory,
const SyncWebSocketFactory& socket_factory,
const Capabilities& capabilities,
std::vector<std::unique_ptr<DevToolsEventListener>>
devtools_event_listeners,
std::unique_ptr<Chrome>* chrome) {
Status status(kOk);
std::unique_ptr<DevToolsHttpClient> devtools_http_client;
bool retry = true;
status = WaitForDevToolsAndCheckVersion(
capabilities.debugger_address, factory, socket_factory, &capabilities, 60,
&devtools_http_client, &retry);
if (status.IsError()) {
return Status(
kUnknownError,
base::StringPrintf("cannot connect to %s at %s",
base::ToLowerASCII(kBrowserShortName).c_str(),
capabilities.debugger_address.ToString().c_str()),
status);
}
std::unique_ptr<DevToolsClient> devtools_websocket_client;
status = CreateBrowserwideDevToolsClientAndConnect(
capabilities.debugger_address, capabilities.perf_logging_prefs,
socket_factory, devtools_event_listeners,
devtools_http_client->browser_info()->web_socket_url,
&devtools_websocket_client);
if (status.IsError()) {
LOG(WARNING) << "Browser-wide DevTools client failed to connect: "
<< status.message();
}
chrome->reset(new ChromeRemoteImpl(
std::move(devtools_http_client), std::move(devtools_websocket_client),
std::move(devtools_event_listeners), capabilities.page_load_strategy));
return Status(kOk);
}
Status LaunchDesktopChrome(network::mojom::URLLoaderFactory* factory,
const SyncWebSocketFactory& socket_factory,
const Capabilities& capabilities,
std::vector<std::unique_ptr<DevToolsEventListener>>
devtools_event_listeners,
std::unique_ptr<Chrome>* chrome,
bool w3c_compliant) {
base::CommandLine command(base::CommandLine::NO_PROGRAM);
base::ScopedTempDir user_data_dir_temp_dir;
base::FilePath user_data_dir;
base::ScopedTempDir extension_dir;
Status status = Status(kOk);
std::vector<std::string> extension_bg_pages;
int devtools_port = 0;
bool retry = true;
if (capabilities.switches.HasSwitch("remote-debugging-port")) {
std::string port_switch =
capabilities.switches.GetSwitchValue("remote-debugging-port");
bool conversion_result = base::StringToInt(port_switch, &devtools_port);
if (!conversion_result || devtools_port < 0 || 65535 < devtools_port) {
return Status(
kUnknownError,
"remote-debugging-port flag has invalid value: " + port_switch);
}
}
if (!devtools_port && capabilities.switches.HasSwitch("user-data-dir")) {
status = internal::RemoveOldDevToolsActivePortFile(base::FilePath(
capabilities.switches.GetSwitchValueNative("user-data-dir")));
if (status.IsError()) {
return status;
}
}
status = PrepareDesktopCommandLine(capabilities, &command,
&user_data_dir_temp_dir, &extension_dir,
&extension_bg_pages, &user_data_dir);
if (status.IsError())
return status;
base::LaunchOptions options;
#if defined(OS_LINUX)
// If minidump path is set in the capability, enable minidump for crashes.
if (!capabilities.minidump_path.empty()) {
VLOG(0) << "Minidump generation specified. Will save dumps to: "
<< capabilities.minidump_path;
options.environment["CHROME_HEADLESS"] = 1;
options.environment["BREAKPAD_DUMP_LOCATION"] = capabilities.minidump_path;
if (!command.HasSwitch(kEnableCrashReport))
command.AppendSwitch(kEnableCrashReport);
}
// We need to allow new privileges so that chrome's setuid sandbox can run.
options.allow_new_privs = true;
#endif
#if !defined(OS_WIN)
if (!capabilities.log_path.empty())
options.environment["CHROME_LOG_FILE"] = capabilities.log_path;
if (capabilities.detach)
options.new_process_group = true;
#endif
#if defined(OS_POSIX)
base::ScopedFD devnull;
const base::CommandLine* cmd_line = base::CommandLine::ForCurrentProcess();
if (!cmd_line->HasSwitch("verbose") &&
cmd_line->GetSwitchValueASCII("log-level") != "ALL") {
// Redirect stderr to /dev/null, so that Chrome log spew doesn't confuse
// users.
devnull.reset(HANDLE_EINTR(open("/dev/null", O_WRONLY)));
if (!devnull.is_valid())
return Status(kUnknownError, "couldn't open /dev/null");
options.fds_to_remap.push_back(
std::make_pair(devnull.get(), STDERR_FILENO));
}
#elif defined(OS_WIN)
if (!SwitchToUSKeyboardLayout())
VLOG(0) << "Cannot switch to US keyboard layout - some keys may be "
"interpreted incorrectly";
#endif
#if defined(OS_WIN)
std::string command_string = base::WideToUTF8(command.GetCommandLineString());
#else
std::string command_string = command.GetCommandLineString();
#endif
VLOG(0) << "Launching " << base::ToLowerASCII(kBrowserShortName) << ": "
<< command_string;
base::Process process = base::LaunchProcess(command, options);
if (!process.IsValid())
return Status(
kUnknownError,
base::StringPrintf("Failed to create %s process.", kBrowserShortName));
// Attempt to connect to devtools in order to send commands to Chrome. If
// attempts fail, check if Chrome has crashed and return error.
std::unique_ptr<DevToolsHttpClient> devtools_http_client;
int exit_code;
base::TerminationStatus chrome_status =
base::TERMINATION_STATUS_STILL_RUNNING;
base::TimeTicks deadline =
base::TimeTicks::Now() + base::TimeDelta::FromSeconds(60);
while (base::TimeTicks::Now() < deadline) {
if (!devtools_port) {
status =
internal::ParseDevToolsActivePortFile(user_data_dir, &devtools_port);
} else {
status = Status(kOk);
}
if (status.IsOk()) {
status = WaitForDevToolsAndCheckVersion(
NetAddress(devtools_port), factory, socket_factory, &capabilities, 1,
&devtools_http_client, &retry);
if (!retry) {
break;
}
}
if (status.IsOk()) {
break;
}
// Check to see if Chrome has crashed.
chrome_status = base::GetTerminationStatus(process.Handle(), &exit_code);
if (chrome_status != base::TERMINATION_STATUS_STILL_RUNNING) {
#if defined(OS_WIN)
if (exit_code == chrome::RESULT_CODE_NORMAL_EXIT_PROCESS_NOTIFIED)
#else
if (WEXITSTATUS(exit_code) ==
chrome::RESULT_CODE_NORMAL_EXIT_PROCESS_NOTIFIED)
#endif
return Status(kInvalidArgument,
"user data directory is already in use, "
"please specify a unique value for --user-data-dir "
"argument, or don't use --user-data-dir");
std::string termination_reason =
internal::GetTerminationReason(chrome_status);
Status failure_status =
Status(kUnknownError, base::StringPrintf("%s failed to start: %s.",
kBrowserShortName,
termination_reason.c_str()));
failure_status.AddDetails(status.message());
// There is a use case of someone passing a path to a binary to us in
// capabilities that is not an actual Chrome binary but a script that
// intercepts our arguments and then starts Chrome itself. This method
// of starting Chrome should be done carefully. The right way to do it
// is to do an exec of Chrome at the end of the script so that Chrome
// remains a subprocess of ChromeDriver. This allows us to have the
// correct process handle so that we can terminate Chrome after the
// test has finished or in the case of any failure. If you can't exec
// the Chrome binary at the end of your script, you must find a way to
// properly handle our termination signal so that you don't have zombie
// Chrome processes running after the test is completed.
failure_status.AddDetails(base::StringPrintf(
"The process started from %s location %s is no longer running, "
"so %s is assuming that %s has crashed.",
base::ToLowerASCII(kBrowserShortName).c_str(),
command.GetProgram().AsUTF8Unsafe().c_str(),
kChromeDriverProductShortName, kBrowserShortName));
return failure_status;
}
base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(50));
}
if (status.IsError()) {
VLOG(0) << "Failed to connect to " << kBrowserShortName
<< ". Attempting to kill it.";
if (!process.Terminate(0, true)) {
int exit_code;
if (base::GetTerminationStatus(process.Handle(), &exit_code) ==
base::TERMINATION_STATUS_STILL_RUNNING)
return Status(kUnknownError,
base::StringPrintf("cannot kill %s", kBrowserShortName),
status);
}
return status;
}
std::unique_ptr<DevToolsClient> devtools_websocket_client;
status = CreateBrowserwideDevToolsClientAndConnect(
NetAddress(devtools_port), capabilities.perf_logging_prefs,
socket_factory, devtools_event_listeners,
devtools_http_client->browser_info()->web_socket_url,
&devtools_websocket_client);
if (status.IsError()) {
LOG(WARNING) << "Browser-wide DevTools client failed to connect: "
<< status.message();
}
std::unique_ptr<ChromeDesktopImpl> chrome_desktop =
std::make_unique<ChromeDesktopImpl>(
std::move(devtools_http_client), std::move(devtools_websocket_client),
std::move(devtools_event_listeners), capabilities.page_load_strategy,
std::move(process), command, &user_data_dir_temp_dir, &extension_dir,
capabilities.network_emulation_enabled);
if (!capabilities.extension_load_timeout.is_zero()) {
for (size_t i = 0; i < extension_bg_pages.size(); ++i) {
VLOG(0) << "Waiting for extension bg page load: "
<< extension_bg_pages[i];
std::unique_ptr<WebView> web_view;
Status status = chrome_desktop->WaitForPageToLoad(
extension_bg_pages[i], capabilities.extension_load_timeout, &web_view,
w3c_compliant);
if (status.IsError()) {
return Status(kUnknownError,
"failed to wait for extension background page to load: " +
extension_bg_pages[i],
status);
}
}
}
*chrome = std::move(chrome_desktop);
return Status(kOk);
}
Status LaunchAndroidChrome(network::mojom::URLLoaderFactory* factory,
const SyncWebSocketFactory& socket_factory,
const Capabilities& capabilities,
std::vector<std::unique_ptr<DevToolsEventListener>>
devtools_event_listeners,
DeviceManager* device_manager,
std::unique_ptr<Chrome>* chrome) {
Status status(kOk);
std::unique_ptr<Device> device;
int devtools_port;
if (capabilities.android_device_serial.empty()) {
status = device_manager->AcquireDevice(&device);
} else {
status = device_manager->AcquireSpecificDevice(
capabilities.android_device_serial, &device);
}
if (status.IsError())
return status;
Switches switches(capabilities.switches);
for (auto* common_switch : kCommonSwitches)
switches.SetUnparsedSwitch(common_switch);
for (auto* android_switch : kAndroidSwitches)
switches.SetUnparsedSwitch(android_switch);
for (auto excluded_switch : capabilities.exclude_switches)
switches.RemoveSwitch(excluded_switch);
status = device->SetUp(
capabilities.android_package, capabilities.android_activity,
capabilities.android_process, capabilities.android_device_socket,
capabilities.android_exec_name, switches.ToString(),
capabilities.android_use_running_app, &devtools_port);
if (status.IsError()) {
device->TearDown();
return status;
}
std::unique_ptr<DevToolsHttpClient> devtools_http_client;
bool retry = true;
status = WaitForDevToolsAndCheckVersion(NetAddress(devtools_port), factory,
socket_factory, &capabilities, 60,
&devtools_http_client, &retry);
if (status.IsError()) {
device->TearDown();
return status;
}
std::unique_ptr<DevToolsClient> devtools_websocket_client;
status = CreateBrowserwideDevToolsClientAndConnect(
NetAddress(devtools_port), capabilities.perf_logging_prefs,
socket_factory, devtools_event_listeners,
devtools_http_client->browser_info()->web_socket_url,
&devtools_websocket_client);
if (status.IsError()) {
LOG(WARNING) << "Browser-wide DevTools client failed to connect: "
<< status.message();
}
chrome->reset(new ChromeAndroidImpl(
std::move(devtools_http_client), std::move(devtools_websocket_client),
std::move(devtools_event_listeners), capabilities.page_load_strategy,
std::move(device)));
return Status(kOk);
}
Status LaunchReplayChrome(network::mojom::URLLoaderFactory* factory,
const SyncWebSocketFactory& socket_factory,
const Capabilities& capabilities,
std::vector<std::unique_ptr<DevToolsEventListener>>
devtools_event_listeners,
std::unique_ptr<Chrome>* chrome,
bool w3c_compliant) {
base::CommandLine command(base::CommandLine::NO_PROGRAM);
base::ScopedTempDir user_data_dir_temp_dir;
base::ScopedTempDir extension_dir;
Status status = Status(kOk);
std::vector<std::string> extension_bg_pages;
if (capabilities.switches.HasSwitch("user-data-dir")) {
status = internal::RemoveOldDevToolsActivePortFile(base::FilePath(
capabilities.switches.GetSwitchValueNative("user-data-dir")));
if (status.IsError()) {
return status;
}
}
#if defined(OS_WIN)
if (!SwitchToUSKeyboardLayout())
VLOG(0) << "Cannot switch to US keyboard layout - some keys may be "
"interpreted incorrectly";
#endif
std::unique_ptr<DevToolsHttpClient> devtools_http_client;
bool retry = true;
status = WaitForDevToolsAndCheckVersion(NetAddress(0), factory,
socket_factory, &capabilities, 1,
&devtools_http_client, &retry);
std::unique_ptr<DevToolsClient> devtools_websocket_client;
status = CreateBrowserwideDevToolsClientAndConnect(
NetAddress(0), capabilities.perf_logging_prefs, socket_factory,
devtools_event_listeners,
devtools_http_client->browser_info()->web_socket_url,
&devtools_websocket_client);
if (status.IsError()) {
LOG(WARNING) << "Browser-wide DevTools client failed to connect: "
<< status.message();
}
base::Process dummy_process;
std::unique_ptr<ChromeDesktopImpl> chrome_impl =
std::make_unique<ChromeReplayImpl>(
std::move(devtools_http_client), std::move(devtools_websocket_client),
std::move(devtools_event_listeners), capabilities.page_load_strategy,
std::move(dummy_process), command, &user_data_dir_temp_dir,
&extension_dir, capabilities.network_emulation_enabled);
if (!capabilities.extension_load_timeout.is_zero()) {
for (size_t i = 0; i < extension_bg_pages.size(); ++i) {
VLOG(0) << "Waiting for extension bg page load: "
<< extension_bg_pages[i];
std::unique_ptr<WebView> web_view;
Status status = chrome_impl->WaitForPageToLoad(
extension_bg_pages[i], capabilities.extension_load_timeout, &web_view,
w3c_compliant);
if (status.IsError()) {
return Status(kUnknownError,
"failed to wait for extension background page to load: " +
extension_bg_pages[i],
status);
}
}
}
*chrome = std::move(chrome_impl);
return Status(kOk);
}
} // namespace
Status LaunchChrome(network::mojom::URLLoaderFactory* factory,
const SyncWebSocketFactory& socket_factory,
DeviceManager* device_manager,
const Capabilities& capabilities,
std::vector<std::unique_ptr<DevToolsEventListener>>
devtools_event_listeners,
std::unique_ptr<Chrome>* chrome,
bool w3c_compliant) {
if (capabilities.IsRemoteBrowser()) {
// TODO(johnchen): Clean up naming for ChromeDriver sessions created
// by connecting to an already-running Chrome at a given debuggerAddress.
return LaunchRemoteChromeSession(factory, socket_factory, capabilities,
std::move(devtools_event_listeners),
chrome);
}
const base::CommandLine* cmd_line = base::CommandLine::ForCurrentProcess();
if (capabilities.IsAndroid()) {
return LaunchAndroidChrome(factory, socket_factory, capabilities,
std::move(devtools_event_listeners),
device_manager, chrome);
} else if (cmd_line->HasSwitch("devtools-replay")) {
return LaunchReplayChrome(factory, socket_factory, capabilities,
std::move(devtools_event_listeners), chrome,
w3c_compliant);
} else {
return LaunchDesktopChrome(factory, socket_factory, capabilities,
std::move(devtools_event_listeners), chrome,
w3c_compliant);
}
}
namespace internal {
void ConvertHexadecimalToIDAlphabet(std::string* id) {
for (size_t i = 0; i < id->size(); ++i) {
int val;
if (base::HexStringToInt(base::StringPiece(id->begin() + i,
id->begin() + i + 1),
&val)) {
(*id)[i] = val + 'a';
} else {
(*id)[i] = 'a';
}
}
}
std::string GenerateExtensionId(const std::string& input) {
uint8_t hash[16];
crypto::SHA256HashString(input, hash, sizeof(hash));
std::string output = base::ToLowerASCII(base::HexEncode(hash, sizeof(hash)));
ConvertHexadecimalToIDAlphabet(&output);
return output;
}
Status GetExtensionBackgroundPage(const base::DictionaryValue* manifest,
const std::string& id,
std::string* bg_page) {
std::string bg_page_name;
bool persistent = true;
manifest->GetBoolean("background.persistent", &persistent);
const base::Value* unused_value;
if (manifest->Get("background.scripts", &unused_value))
bg_page_name = "_generated_background_page.html";
manifest->GetString("background.page", &bg_page_name);
if (bg_page_name.empty() || !persistent)
return Status(kOk);
GURL baseUrl("chrome-extension://" + id + "/");
*bg_page = baseUrl.Resolve(bg_page_name).spec();
return Status(kOk);
}
Status ProcessExtension(const std::string& extension,
const base::FilePath& temp_dir,
base::FilePath* path,
std::string* bg_page) {
// Decodes extension string.
// Some WebDriver client base64 encoders follow RFC 1521, which require that
// 'encoded lines be no more than 76 characters long'. Just remove any
// newlines.
std::string extension_base64;
base::RemoveChars(extension, "\n", &extension_base64);
std::string decoded_extension;
if (!base::Base64Decode(extension_base64, &decoded_extension))
return Status(kUnknownError, "cannot base64 decode");
base::ScopedTempDir temp_crx_dir;
if (!temp_crx_dir.CreateUniqueTempDir())
return Status(kUnknownError, "cannot create temp dir");
base::FilePath extension_crx = temp_crx_dir.GetPath().AppendASCII("temp.crx");
int size = static_cast<int>(decoded_extension.length());
if (base::WriteFile(extension_crx, decoded_extension.c_str(), size) != size) {
return Status(kUnknownError, "cannot write file");
}
// If the file is a crx file, extract the extension's ID from its public key.
// Otherwise generate a random public key and use its derived extension ID.
std::string public_key_base64;
std::string magic_header = decoded_extension.substr(0, 4);
if (magic_header.size() != 4)
return Status(kUnknownError, "cannot extract magic number");
const bool is_crx_file = magic_header == "Cr24";
std::string id;
if (is_crx_file) {
crx_file::VerifierResult result =
crx_file::Verify(extension_crx, crx_file::VerifierFormat::CRX3,
{} /** required_key_hashes */,
{} /** required_file_hash */, &public_key_base64, &id);
if (result != crx_file::VerifierResult::OK_FULL) {
return Status(kUnknownError,
base::StringPrintf("CRX verification failed: %d", result));
}
} else {
// Not a CRX file. Generate RSA keypair to get a valid extension id.
std::unique_ptr<crypto::RSAPrivateKey> key_pair(
crypto::RSAPrivateKey::Create(2048));
if (!key_pair)
return Status(kUnknownError, "cannot generate RSA key pair");
std::vector<uint8_t> public_key_vector;
if (!key_pair->ExportPublicKey(&public_key_vector))
return Status(kUnknownError, "cannot extract public key");
std::string public_key =
std::string(reinterpret_cast<char*>(&public_key_vector.front()),
public_key_vector.size());
id = GenerateExtensionId(public_key);
base::Base64Encode(public_key, &public_key_base64);
}
// Unzip the crx file.
base::FilePath extension_dir = temp_dir.AppendASCII("extension_" + id);
if (!zip::Unzip(extension_crx, extension_dir))
return Status(kUnknownError, "cannot unzip");
// Parse the manifest and set the 'key' if not already present.
base::FilePath manifest_path(extension_dir.AppendASCII("manifest.json"));
std::string manifest_data;
if (!base::ReadFileToString(manifest_path, &manifest_data))
return Status(kUnknownError, "cannot read manifest");
std::unique_ptr<base::Value> manifest_value =
base::JSONReader::ReadDeprecated(manifest_data);
base::DictionaryValue* manifest;
if (!manifest_value || !manifest_value->GetAsDictionary(&manifest))
return Status(kUnknownError, "invalid manifest");
std::string manifest_key_base64;
if (manifest->GetString("key", &manifest_key_base64)) {
// If there is a key in both the header and the manifest, use the key in the
// manifest. This allows chromedriver users users who generate dummy crxs
// to set the manifest key and have a consistent ID.
std::string manifest_key;
if (!base::Base64Decode(manifest_key_base64, &manifest_key))
return Status(kUnknownError, "'key' in manifest is not base64 encoded");
std::string manifest_id = GenerateExtensionId(manifest_key);
if (id != manifest_id) {
if (is_crx_file) {
LOG(WARNING)
<< "Public key in crx header is different from key in manifest"
<< std::endl << "key from header: " << public_key_base64
<< std::endl << "key from manifest: " << manifest_key_base64
<< std::endl << "generated extension id from header key: " << id
<< std::endl << "generated extension id from manifest key: "
<< manifest_id;
}
id = manifest_id;
}
} else {
manifest->SetString("key", public_key_base64);
base::JSONWriter::Write(*manifest, &manifest_data);
if (base::WriteFile(
manifest_path, manifest_data.c_str(), manifest_data.size()) !=
static_cast<int>(manifest_data.size())) {
return Status(kUnknownError, "cannot add 'key' to manifest");
}
}
// Get extension's background page URL, if there is one.
std::string bg_page_tmp;
Status status = GetExtensionBackgroundPage(manifest, id, &bg_page_tmp);
if (status.IsError())
return status;
*path = extension_dir;
if (bg_page_tmp.size())
*bg_page = bg_page_tmp;
return Status(kOk);
}
void UpdateExtensionSwitch(Switches* switches,
const char name[],
const base::FilePath::StringType& extension) {
base::FilePath::StringType value = switches->GetSwitchValueNative(name);
if (value.length())
value += FILE_PATH_LITERAL(",");
value += extension;
switches->SetSwitch(name, value);
}
Status ProcessExtensions(const std::vector<std::string>& extensions,
const base::FilePath& temp_dir,
bool include_automation_extension,
Switches* switches,
std::vector<std::string>* bg_pages) {
std::vector<std::string> bg_pages_tmp;
std::vector<base::FilePath::StringType> extension_paths;
for (size_t i = 0; i < extensions.size(); ++i) {
base::FilePath path;
std::string bg_page;
Status status = ProcessExtension(extensions[i], temp_dir, &path, &bg_page);
if (status.IsError()) {
return Status(
kUnknownError,
base::StringPrintf("cannot process extension #%" PRIuS, i + 1),
status);
}
extension_paths.push_back(path.value());
if (bg_page.length())
bg_pages_tmp.push_back(bg_page);
}
if (include_automation_extension) {
base::FilePath automation_extension;
Status status = UnpackAutomationExtension(temp_dir, &automation_extension);
if (status.IsError())
return status;
if (switches->HasSwitch("disable-extensions")) {
UpdateExtensionSwitch(switches, "disable-extensions-except",
automation_extension.value());
} else {
extension_paths.push_back(automation_extension.value());
}
}
if (extension_paths.size()) {
base::FilePath::StringType extension_paths_value = base::JoinString(
extension_paths, base::FilePath::StringType(1, ','));
UpdateExtensionSwitch(switches, "load-extension", extension_paths_value);
}
bg_pages->swap(bg_pages_tmp);
return Status(kOk);
}
Status WritePrefsFile(
const std::string& template_string,
const base::DictionaryValue* custom_prefs,
const base::FilePath& path) {
int code;
std::string error_msg;
std::unique_ptr<base::Value> template_value =
base::JSONReader::ReadAndReturnErrorDeprecated(template_string, 0, &code,
&error_msg);
base::DictionaryValue* prefs;
if (!template_value || !template_value->GetAsDictionary(&prefs)) {
return Status(kUnknownError,
"cannot parse internal JSON template: " + error_msg);
}
if (custom_prefs) {
for (base::DictionaryValue::Iterator it(*custom_prefs); !it.IsAtEnd();
it.Advance()) {
prefs->Set(it.key(), std::make_unique<base::Value>(it.value().Clone()));
}
}
std::string prefs_str;
base::JSONWriter::Write(*prefs, &prefs_str);
VLOG(0) << "Populating " << path.BaseName().value()
<< " file: " << PrettyPrintValue(*prefs);
if (static_cast<int>(prefs_str.length()) != base::WriteFile(
path, prefs_str.c_str(), prefs_str.length())) {
return Status(kUnknownError, "failed to write prefs file");
}
return Status(kOk);
}
Status PrepareUserDataDir(
const base::FilePath& user_data_dir,
const base::DictionaryValue* custom_prefs,
const base::DictionaryValue* custom_local_state) {
base::FilePath default_dir =
user_data_dir.AppendASCII(chrome::kInitialProfile);
if (!base::CreateDirectory(default_dir))
return Status(kUnknownError, "cannot create default profile directory");
std::string preferences;
base::FilePath preferences_path =
default_dir.Append(chrome::kPreferencesFilename);
if (base::PathExists(preferences_path))
base::ReadFileToString(preferences_path, &preferences);
else
preferences = kPreferences;
Status status =
WritePrefsFile(preferences,
custom_prefs,
default_dir.Append(chrome::kPreferencesFilename));
if (status.IsError())
return status;
std::string local_state;
base::FilePath local_state_path =
user_data_dir.Append(chrome::kLocalStateFilename);
if (base::PathExists(local_state_path))
base::ReadFileToString(local_state_path, &local_state);
else
local_state = kLocalState;
status = WritePrefsFile(local_state,
custom_local_state,
user_data_dir.Append(chrome::kLocalStateFilename));
if (status.IsError())
return status;
// Write empty "First Run" file, otherwise Chrome will wipe the default
// profile that was written.
if (base::WriteFile(
user_data_dir.Append(chrome::kFirstRunSentinel), "", 0) != 0) {
return Status(kUnknownError, "failed to write first run file");
}
return Status(kOk);
}
Status ParseDevToolsActivePortFile(const base::FilePath& user_data_dir,
int* port) {
base::FilePath port_filepath = user_data_dir.Append(kDevToolsActivePort);
if (!base::PathExists(port_filepath)) {
return Status(kUnknownError, "DevToolsActivePort file doesn't exist");
}
std::string buffer;
bool result = base::ReadFileToString(port_filepath, &buffer);
if (!result) {
return Status(kUnknownError, "Could not read in devtools port number");
}
std::vector<std::string> split_port_strings = base::SplitString(
buffer, "\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
if (split_port_strings.size() < 2) {
return Status(kUnknownError,
std::string("Devtools port number file contents <") + buffer +
std::string("> were in an unexpected format"));
}
if (!base::StringToInt(split_port_strings.front(), port)) {
return Status(kUnknownError,
"Could not convert devtools port number to int");
}
return Status(kOk);
}
Status RemoveOldDevToolsActivePortFile(const base::FilePath& user_data_dir) {
base::FilePath port_filepath = user_data_dir.Append(kDevToolsActivePort);
// Note that calling DeleteFile on a path that doesn't exist returns True.
if (base::DeleteFile(port_filepath, false)) {
return Status(kOk);
}
return Status(
kUnknownError,
base::StringPrintf(
"Could not remove old devtools port file. Perhaps the given "
"user-data-dir at %s is still attached to a running %s or "
"Chromium process",
user_data_dir.AsUTF8Unsafe().c_str(), kBrowserShortName));
}
std::string GetTerminationReason(base::TerminationStatus status) {
switch (status) {
case base::TERMINATION_STATUS_NORMAL_TERMINATION:
return "exited normally";
case base::TERMINATION_STATUS_ABNORMAL_TERMINATION:
return "exited abnormally";
case base::TERMINATION_STATUS_PROCESS_WAS_KILLED:
#if defined(OS_CHROMEOS)
case base::TERMINATION_STATUS_PROCESS_WAS_KILLED_BY_OOM:
#endif
return "was killed";
case base::TERMINATION_STATUS_PROCESS_CRASHED:
return "crashed";
case base::TERMINATION_STATUS_LAUNCH_FAILED:
return "failed to launch";
default:
return "unknown";
}
}
} // namespace internal