diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2017-04-05 17:15:33 +0200 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2017-04-11 07:47:18 +0000 |
commit | 7324afb043a0b1e623d8e8eb906cdc53bdeb4685 (patch) | |
tree | a3fe2d74ea9c9e142c390dac4ca0e219382ace46 /chromium/headless | |
parent | 6a4cabb866f66d4128a97cdc6d9d08ce074f1247 (diff) | |
download | qtwebengine-chromium-7324afb043a0b1e623d8e8eb906cdc53bdeb4685.tar.gz |
BASELINE: Update Chromium to 58.0.3029.54
Change-Id: I67f57065a7afdc8e4614adb5c0230281428df4d1
Reviewed-by: Peter Varga <pvarga@inf.u-szeged.hu>
Diffstat (limited to 'chromium/headless')
76 files changed, 3716 insertions, 656 deletions
diff --git a/chromium/headless/BUILD.gn b/chromium/headless/BUILD.gn index d6cd10639cd..bc66c58443b 100644 --- a/chromium/headless/BUILD.gn +++ b/chromium/headless/BUILD.gn @@ -3,6 +3,8 @@ # found in the LICENSE file. import("//build/config/chrome_build.gni") +import("//headless/headless.gni") +import("//build/util/process_version.gni") import("//mojo/public/tools/bindings/mojom.gni") import("//testing/test.gni") import("//tools/grit/grit_rule.gni") @@ -10,6 +12,10 @@ import("//tools/grit/repack.gni") config("headless_implementation") { defines = [ "HEADLESS_IMPLEMENTATION" ] + + if (headless_use_embedded_resources) { + defines += [ "HEADLESS_USE_EMBEDDED_RESOURCES" ] + } } group("headless") { @@ -20,12 +26,12 @@ group("headless") { repack("pak") { sources = [ - "$root_gen_dir/blink/devtools_resources.pak", "$root_gen_dir/blink/public/resources/blink_image_resources_100_percent.pak", "$root_gen_dir/blink/public/resources/blink_resources.pak", "$root_gen_dir/components/strings/components_strings_en-US.pak", "$root_gen_dir/content/app/resources/content_resources_100_percent.pak", "$root_gen_dir/content/app/strings/content_strings_en-US.pak", + "$root_gen_dir/content/browser/devtools/devtools_resources.pak", "$root_gen_dir/content/browser/tracing/tracing_resources.pak", "$root_gen_dir/content/content_resources.pak", "$root_gen_dir/headless/headless_lib_resources.pak", @@ -60,6 +66,39 @@ repack("pak") { output = "$root_out_dir/headless_lib.pak" } +action("embed_resources") { + # TODO(altimin): Consider zipping file here, it can reduce size up to 80%. + script = "//headless/lib/embed_data.py" + + inputs = [ + "$root_out_dir/headless_lib.pak", + ] + + outputs = [ + "$root_gen_dir/headless/embedded_resource_pak.cc", + "$root_gen_dir/headless/embedded_resource_pak.h", + ] + + args = [ + "--data_file", + rebase_path("$root_out_dir/headless_lib.pak"), + "--gendir", + rebase_path("$root_gen_dir"), + "--header_file", + "headless/embedded_resource_pak.h", + "--source_file", + "headless/embedded_resource_pak.cc", + "--namespace", + "headless", + "--variable_name", + "kHeadlessResourcePak", + ] + + deps = [ + ":pak", + ] +} + grit("resources") { source = "lib/resources/headless_lib_resources.grd" outputs = [ @@ -157,6 +196,7 @@ static_library("headless_lib") { "lib/browser/headless_browser_context_options.h", "lib/browser/headless_browser_impl.cc", "lib/browser/headless_browser_impl.h", + "lib/browser/headless_browser_impl_mac.mm", "lib/browser/headless_browser_main_parts.cc", "lib/browser/headless_browser_main_parts.h", "lib/browser/headless_content_browser_client.cc", @@ -167,18 +207,15 @@ static_library("headless_lib") { "lib/browser/headless_devtools_client_impl.h", "lib/browser/headless_devtools_manager_delegate.cc", "lib/browser/headless_devtools_manager_delegate.h", + "lib/browser/headless_macros.h", "lib/browser/headless_platform_event_source.cc", "lib/browser/headless_platform_event_source.h", - "lib/browser/headless_screen.cc", - "lib/browser/headless_screen.h", "lib/browser/headless_url_request_context_getter.cc", "lib/browser/headless_url_request_context_getter.h", "lib/browser/headless_web_contents_impl.cc", "lib/browser/headless_web_contents_impl.h", - "lib/browser/headless_window_parenting_client.cc", - "lib/browser/headless_window_parenting_client.h", - "lib/browser/headless_window_tree_host.cc", - "lib/browser/headless_window_tree_host.h", + "lib/headless_crash_reporter_client.cc", + "lib/headless_crash_reporter_client.h", "lib/headless_content_client.cc", "lib/headless_content_client.h", "lib/headless_content_main_delegate.cc", @@ -207,6 +244,8 @@ static_library("headless_lib") { "public/util/expedited_dispatcher.h", "public/util/generic_url_request_job.cc", "public/util/generic_url_request_job.h", + "public/util/flat_dom_tree_extractor.cc", + "public/util/flat_dom_tree_extractor.h", "public/util/http_url_fetcher.cc", "public/util/http_url_fetcher.h", "public/util/in_memory_protocol_handler.cc", @@ -215,6 +254,7 @@ static_library("headless_lib") { "public/util/in_memory_request_job.h", "public/util/managed_dispatch_url_request_job.cc", "public/util/managed_dispatch_url_request_job.h", + "public/util/navigation_request.h", "public/util/testing/generic_url_request_mocks.cc", "public/util/testing/generic_url_request_mocks.h", "public/util/url_fetcher.cc", @@ -224,20 +264,35 @@ static_library("headless_lib") { "public/util/user_agent.h", ] + if (use_aura) { + sources += [ + "lib/browser/headless_browser_impl_aura.cc", + "lib/browser/headless_focus_client.cc", + "lib/browser/headless_focus_client.h", + "lib/browser/headless_screen.cc", + "lib/browser/headless_screen.h", + "lib/browser/headless_window_parenting_client.cc", + "lib/browser/headless_window_parenting_client.h", + "lib/browser/headless_window_tree_host.cc", + "lib/browser/headless_window_tree_host.h", + ] + } + deps = [ ":gen_devtools_client_api", - ":pak", + ":version_header", "//base", + "//components/crash/content/browser", "//components/security_state/content", "//components/security_state/core", "//content/public/app:both", "//content/public/browser", + "//content/public/child:child", "//content/public/common", "//content/public/common:service_names", "//net", "//services/service_manager/public/cpp", "//third_party/mesa:osmesa", - "//ui/aura", "//ui/base", "//ui/compositor", "//ui/display", @@ -245,6 +300,28 @@ static_library("headless_lib") { "//url", ] + if (is_win) { + deps += [ + "//build/win:default_exe_manifest", + "//content:sandbox_helper_win", + "//sandbox", + ] + } + + if (!is_mac) { + deps += [ "//ui/aura" ] + } + + if (headless_use_embedded_resources) { + deps += [ ":embed_resources" ] + sources += [ + "$root_gen_dir/headless/embedded_resource_pak.cc", + "$root_gen_dir/headless/embedded_resource_pak.h", + ] + } else { + deps += [ ":pak" ] + } + if (use_ozone) { deps += [ "//ui/ozone" ] } @@ -257,6 +334,7 @@ group("headless_tests") { deps = [ ":headless_browsertests", + ":headless_example", ":headless_unittests", ] } @@ -321,6 +399,8 @@ test("headless_browsertests") { "lib/headless_devtools_client_browsertest.cc", "lib/headless_web_contents_browsertest.cc", "public/util/dom_tree_extractor_browsertest.cc", + "public/util/flat_dom_tree_extractor_browsertest.cc", + "public/util/protocol_handler_request_id_browsertest.cc", "test/headless_browser_test.cc", "test/headless_browser_test.h", "test/headless_test_launcher.cc", @@ -357,14 +437,19 @@ test("headless_browsertests") { static_library("headless_shell_lib") { sources = [ "app/headless_shell.cc", + "app/headless_shell.h", "app/headless_shell_switches.cc", "app/headless_shell_switches.h", + "app/shell_navigation_request.cc", + "app/shell_navigation_request.h", "public/headless_shell.h", ] deps = [ "//headless:headless_lib", ] + + configs += [ ":headless_implementation" ] } executable("headless_shell") { @@ -375,4 +460,25 @@ executable("headless_shell") { deps = [ "//headless:headless_shell_lib", ] + + configs += [ ":headless_implementation" ] +} + +process_version("version_header") { + template_file = "public/version.h.in" + sources = [ + "//build/util/LASTCHANGE", + "//chrome/VERSION", + ] + output = "$target_gen_dir/public/version.h" +} + +executable("headless_example") { + sources = [ + "app/headless_example.cc", + ] + + deps = [ + "//headless:headless_shell_lib", + ] } diff --git a/chromium/headless/DEPS b/chromium/headless/DEPS index edf61b6ede4..2e51ac4eae3 100644 --- a/chromium/headless/DEPS +++ b/chromium/headless/DEPS @@ -1,4 +1,6 @@ include_rules = [ + "+components/crash/content/app", + "+components/crash/content/browser", "+content/public/app", "+content/public/browser", "+content/public/common", diff --git a/chromium/headless/OWNERS b/chromium/headless/OWNERS index c87978e7381..af4e1d8ec38 100644 --- a/chromium/headless/OWNERS +++ b/chromium/headless/OWNERS @@ -1,3 +1,6 @@ skyostil@chromium.org alexclarke@chromium.org altimin@chromium.org +eseckler@chromium.org + +# TEAM: headless-dev@chromium.org diff --git a/chromium/headless/README.md b/chromium/headless/README.md index bf4d30149cf..eebede197f5 100644 --- a/chromium/headless/README.md +++ b/chromium/headless/README.md @@ -23,6 +23,37 @@ missing Mesa library. [DevTools](https://developer.chrome.com/devtools) interface or use a tool such as [Selenium](http://www.seleniumhq.org/) to drive the headless browser. +## Usage from Node.js + +For example, the [chrome-remote-interface](https://github.com/cyrus-and/chrome-remote-interface) +Node.js package can be used to extract a page's DOM like this: + +``` +const CDP = require('chrome-remote-interface'); + +CDP((client) => { + // Extract used DevTools domains. + const {Page, Runtime} = client; + + // Enable events on domains we are interested in. + Promise.all([ + Page.enable() + ]).then(() => { + return Page.navigate({url: 'https://example.com'}); + }); + + // Evaluate outerHTML after page has loaded. + Page.loadEventFired(() => { + Runtime.evaluate({expression: 'document.body.outerHTML'}).then((result) => { + console.log(result.result.value); + client.close(); + }); + }); +}).on('error', (err) => { + console.error('Cannot connect to browser:', err); +}); +``` + ## Usage as a C++ library Headless Chromium can be built as a library for embedding into a C++ @@ -30,8 +61,10 @@ application. This approach is otherwise similar to controlling the browser over a DevTools connection, but it provides more customization points, e.g., for networking and [mojo services](https://docs.google.com/document/d/1Fr6_DJH6OK9rG3-ibMvRPTNnHsAXPk0VzxxiuJDSK3M/edit#heading=h.qh0udvlk963d). -Headless Shell is a sample application which demonstrates the use of the -headless C++ API. To run it, first initialize a headless build configuration: +[Headless Example](https://cs.chromium.org/chromium/src/headless/app/headless_example.cc) +is a small sample application which demonstrates the use of the headless C++ +API. It loads a web page and outputs the resulting DOM. To run it, first +initialize a headless build configuration: ``` $ mkdir -p out/Debug @@ -39,22 +72,26 @@ $ echo 'import("//build/args/headless.gn")' > out/Debug/args.gn $ gn gen out/Debug ``` -Then build the shell: +Then build the example: ``` -$ ninja -C out/Debug headless_shell +$ ninja -C out/Debug headless_example ``` -After the build completes, Headless Shell can be run with the following command: +After the build completes, the example can be run with the following command: ``` -$ out/Debug/headless_shell https://www.google.com +$ out/Debug/headless_example https://www.google.com ``` -To attach a [DevTools](https://developer.chrome.com/devtools) debugger to the -shell, start it with an argument specifying the debugging port: +[Headless Shell](https://cs.chromium.org/chromium/src/headless/app/headless_shell.cc) +is a more capable headless application. For instance, it supports remote +debugging with the [DevTools](https://developer.chrome.com/devtools) protocol. +To do this, start the application with an argument specifying the debugging +port: ``` +$ ninja -C out/Debug headless_shell $ out/Debug/headless_shell --remote-debugging-port=9222 https://youtube.com ``` @@ -89,8 +126,14 @@ web pages. Its main classes are: ## Resources and Documentation Mailing list: [headless-dev@chromium.org](https://groups.google.com/a/chromium.org/forum/#!forum/headless-dev) + Bug tracker: [Proj=Headless](https://bugs.chromium.org/p/chromium/issues/list?can=2&q=Proj%3DHeadless) +[File a new bug](https://bugs.chromium.org/p/chromium/issues/entry?labels=Proj-Headless) + +* [BeginFrame sequence numbers + acknowledgements](https://docs.google.com/document/d/1nxaunQ0cYWxhtS6Zzfwa99nae74F7gxanbuT5JRpI6Y/edit#) +* [Deterministic page loading for Blink](https://docs.google.com/document/d/19s2g4fPP9p9qmMZvwPX8uDGbb-39rgR9k56B4B-ueG8/edit#) +* [Crash dumps for Headless Chrome](https://docs.google.com/document/d/1l6AGOOBLk99PaAKoZQW_DVhM8FQ6Fut27lD938CRbTM/edit) * [Runtime headless mode for Chrome](https://docs.google.com/document/d/1aIJUzQr3eougZQp90bp4mqGr5gY6hdUice8UPa-Ys90/edit#) * [Virtual Time in Blink](https://docs.google.com/document/d/1y9kdt_zezt7pbey6uzvt1dgklwc1ob_vy4nzo1zbqmo/edit#heading=h.tn3gd1y9ifml) * [Headless Chrome architecture](https://docs.google.com/document/d/11zIkKkLBocofGgoTeeyibB2TZ_k7nR78v7kNelCatUE/edit) diff --git a/chromium/headless/app/headless_example.cc b/chromium/headless/app/headless_example.cc new file mode 100644 index 00000000000..56932ef45ba --- /dev/null +++ b/chromium/headless/app/headless_example.cc @@ -0,0 +1,180 @@ +// Copyright 2017 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. + +// A small example application showing the use of the C++ Headless Chrome +// library. It navigates to a web site given on the command line, waits for it +// to load and prints out the DOM. +// +// Tip: start reading from the main() function below. + +#include "base/bind.h" +#include "base/command_line.h" +#include "base/memory/weak_ptr.h" +#include "headless/public/devtools/domains/page.h" +#include "headless/public/devtools/domains/runtime.h" +#include "headless/public/headless_browser.h" +#include "headless/public/headless_devtools_client.h" +#include "headless/public/headless_devtools_target.h" +#include "headless/public/headless_web_contents.h" +#include "ui/gfx/geometry/size.h" + +// This class contains the main application logic, i.e., waiting for a page to +// load and printing its DOM. Note that browser initialization happens outside +// this class. +class HeadlessExample : public headless::HeadlessWebContents::Observer, + public headless::page::Observer { + public: + HeadlessExample(headless::HeadlessBrowser* browser, + headless::HeadlessWebContents* web_contents); + ~HeadlessExample() override; + + // headless::HeadlessWebContents::Observer implementation: + void DevToolsTargetReady() override; + + // headless::page::Observer implementation: + void OnLoadEventFired( + const headless::page::LoadEventFiredParams& params) override; + + // Tip: Observe headless::inspector::ExperimentalObserver::OnTargetCrashed to + // be notified of renderer crashes. + + void OnDomFetched(std::unique_ptr<headless::runtime::EvaluateResult> result); + + private: + // The headless browser instance. Owned by the headless library. See main(). + headless::HeadlessBrowser* browser_; + // Our tab. Owned by |browser_|. + headless::HeadlessWebContents* web_contents_; + // The DevTools client used to control the tab. + std::unique_ptr<headless::HeadlessDevToolsClient> devtools_client_; + // A helper for creating weak pointers to this class. + base::WeakPtrFactory<HeadlessExample> weak_factory_; +}; + +namespace { +HeadlessExample* g_example; +} + +HeadlessExample::HeadlessExample(headless::HeadlessBrowser* browser, + headless::HeadlessWebContents* web_contents) + : browser_(browser), + web_contents_(web_contents), + devtools_client_(headless::HeadlessDevToolsClient::Create()), + weak_factory_(this) { + web_contents_->AddObserver(this); +} + +HeadlessExample::~HeadlessExample() { + // Note that we shut down the browser last, because it owns objects such as + // the web contents which can no longer be accessed after the browser is gone. + devtools_client_->GetPage()->RemoveObserver(this); + web_contents_->GetDevToolsTarget()->DetachClient(devtools_client_.get()); + web_contents_->RemoveObserver(this); + browser_->Shutdown(); +} + +// This method is called when the tab is ready for DevTools inspection. +void HeadlessExample::DevToolsTargetReady() { + // Attach our DevTools client to the tab so that we can send commands to it + // and observe events. + web_contents_->GetDevToolsTarget()->AttachClient(devtools_client_.get()); + + // Start observing events from DevTools's page domain. This lets us get + // notified when the page has finished loading. Note that it is possible + // the page has already finished loading by now. See + // HeadlessShell::DevToolTargetReady for how to handle that case correctly. + devtools_client_->GetPage()->AddObserver(this); + devtools_client_->GetPage()->Enable(); +} + +void HeadlessExample::OnLoadEventFired( + const headless::page::LoadEventFiredParams& params) { + // The page has now finished loading. Let's grab a snapshot of the DOM by + // evaluating the outerHTML property on the body element. + devtools_client_->GetRuntime()->Evaluate( + "document.body.outerHTML", + base::Bind(&HeadlessExample::OnDomFetched, weak_factory_.GetWeakPtr())); +} + +void HeadlessExample::OnDomFetched( + std::unique_ptr<headless::runtime::EvaluateResult> result) { + std::string dom; + // Make sure the evaluation succeeded before reading the result. + if (result->HasExceptionDetails()) { + LOG(ERROR) << "Failed to evaluate document.body.outerHTML: " + << result->GetExceptionDetails()->GetText(); + } else if (result->GetResult()->GetValue()->GetAsString(&dom)) { + printf("%s\n", dom.c_str()); + } + + // Shut down the browser (see ~HeadlessExample). + delete g_example; + g_example = nullptr; +} + +// This function is called by the headless library after the browser has been +// initialized. It runs on the UI thread. +void OnHeadlessBrowserStarted(headless::HeadlessBrowser* browser) { + // In order to open tabs, we first need a browser context. It corresponds to a + // user profile and contains things like the user's cookies, local storage, + // cache, etc. + headless::HeadlessBrowserContext::Builder context_builder = + browser->CreateBrowserContextBuilder(); + + // Here we can set options for the browser context. As an example we enable + // incognito mode, which makes sure profile data is not written to disk. + context_builder.SetIncognitoMode(true); + + // Construct the context and set it as the default. The default browser + // context is used by the Target.createTarget() DevTools command when no other + // context is given. + headless::HeadlessBrowserContext* browser_context = context_builder.Build(); + browser->SetDefaultBrowserContext(browser_context); + + // Get the URL from the command line. + base::CommandLine::StringVector args = + base::CommandLine::ForCurrentProcess()->GetArgs(); + if (args.empty()) { + LOG(ERROR) << "No URL to load"; + browser->Shutdown(); + return; + } + GURL url(args[0]); + + // Open a tab (i.e., HeadlessWebContents) in the newly created browser + // context. + headless::HeadlessWebContents::Builder tab_builder( + browser_context->CreateWebContentsBuilder()); + + // We can set options for the opened tab here. In this example we are just + // setting the initial URL to navigate to. + tab_builder.SetInitialURL(url); + + // Create an instance of the example app, which will wait for the page to load + // and print its DOM. + headless::HeadlessWebContents* web_contents = tab_builder.Build(); + g_example = new HeadlessExample(browser, web_contents); +} + +int main(int argc, const char** argv) { + // This function must be the first thing we call to make sure child processes + // such as the renderer are started properly. The headless library starts + // child processes by forking and exec'ing the main application. + headless::RunChildProcessIfNeeded(argc, argv); + + // Create a headless browser instance. There can be one of these per process + // and it can only be initialized once. + headless::HeadlessBrowser::Options::Builder builder(argc, argv); + + // Here you can customize browser options. As an example we set the window + // size. + builder.SetWindowSize(gfx::Size(800, 600)); + + // Pass control to the headless library. It will bring up the browser and + // invoke the given callback on the browser UI thread. Note: if you need to + // pass more parameters to the callback, you can add them to the Bind() call + // below. + return headless::HeadlessBrowserMain(builder.Build(), + base::Bind(&OnHeadlessBrowserStarted)); +} diff --git a/chromium/headless/app/headless_shell.cc b/chromium/headless/app/headless_shell.cc index 047f408a0f4..7c187b00a82 100644 --- a/chromium/headless/app/headless_shell.cc +++ b/chromium/headless/app/headless_shell.cc @@ -7,30 +7,20 @@ #include <string> #include "base/base64.h" +#include "base/base_switches.h" #include "base/bind.h" #include "base/callback.h" #include "base/command_line.h" #include "base/files/file_path.h" #include "base/json/json_writer.h" #include "base/location.h" -#include "base/memory/ptr_util.h" -#include "base/memory/ref_counted.h" #include "base/memory/weak_ptr.h" #include "base/numerics/safe_conversions.h" #include "base/strings/string_number_conversions.h" -#include "content/public/common/content_switches.h" +#include "headless/app/headless_shell.h" #include "headless/app/headless_shell_switches.h" -#include "headless/public/devtools/domains/emulation.h" -#include "headless/public/devtools/domains/inspector.h" -#include "headless/public/devtools/domains/page.h" -#include "headless/public/devtools/domains/runtime.h" -#include "headless/public/headless_browser.h" -#include "headless/public/headless_devtools_client.h" #include "headless/public/headless_devtools_target.h" -#include "headless/public/headless_web_contents.h" -#include "headless/public/util/deterministic_dispatcher.h" #include "headless/public/util/deterministic_http_protocol_handler.h" -#include "net/base/file_stream.h" #include "net/base/io_buffer.h" #include "net/base/ip_address.h" #include "net/base/net_errors.h" @@ -55,361 +45,358 @@ bool ParseWindowSize(std::string window_size, gfx::Size* parsed_window_size) { } } // namespace -// An application which implements a simple headless browser. -class HeadlessShell : public HeadlessWebContents::Observer, - emulation::ExperimentalObserver, - inspector::ExperimentalObserver, - page::Observer { - public: - HeadlessShell() - : browser_(nullptr), - devtools_client_(HeadlessDevToolsClient::Create()), - web_contents_(nullptr), - processed_page_ready_(false), - browser_context_(nullptr), - weak_factory_(this) {} - ~HeadlessShell() override {} - - void OnStart(HeadlessBrowser* browser) { - browser_ = browser; - - HeadlessBrowserContext::Builder context_builder = - browser_->CreateBrowserContextBuilder(); - // TODO(eseckler): These switches should also affect BrowserContexts that - // are created via DevTools later. - if (base::CommandLine::ForCurrentProcess()->HasSwitch( - switches::kDeterministicFetch)) { - deterministic_dispatcher_.reset( - new DeterministicDispatcher(browser_->BrowserIOThread())); - - ProtocolHandlerMap protocol_handlers; - protocol_handlers[url::kHttpScheme] = - base::MakeUnique<DeterministicHttpProtocolHandler>( - deterministic_dispatcher_.get(), browser->BrowserIOThread()); - protocol_handlers[url::kHttpsScheme] = - base::MakeUnique<DeterministicHttpProtocolHandler>( - deterministic_dispatcher_.get(), browser->BrowserIOThread()); - - context_builder.SetProtocolHandlers(std::move(protocol_handlers)); - } - if (base::CommandLine::ForCurrentProcess()->HasSwitch( - switches::kHideScrollbars)) { - context_builder.SetOverrideWebPreferencesCallback( - base::Bind([](WebPreferences* preferences) { - preferences->hide_scrollbars = true; - })); - } - browser_context_ = context_builder.Build(); - browser_->SetDefaultBrowserContext(browser_context_); - - HeadlessWebContents::Builder builder( - browser_context_->CreateWebContentsBuilder()); - base::CommandLine::StringVector args = - base::CommandLine::ForCurrentProcess()->GetArgs(); - - // TODO(alexclarke): Should we navigate to about:blank first if using - // virtual time? - if (args.empty()) - args.push_back("about:blank"); - for (auto it = args.rbegin(); it != args.rend(); ++it) { - GURL url(*it); - HeadlessWebContents* web_contents = builder.SetInitialURL(url).Build(); - if (!web_contents) { - LOG(ERROR) << "Navigation to " << url << " failed"; - browser_->Shutdown(); - return; - } - if (!web_contents_ && !RemoteDebuggingEnabled()) { - // TODO(jzfeng): Support observing multiple targets. - url_ = url; - web_contents_ = web_contents; - web_contents_->AddObserver(this); - } - } - } - - void Shutdown() { - if (!web_contents_) +HeadlessShell::HeadlessShell() + : browser_(nullptr), + devtools_client_(HeadlessDevToolsClient::Create()), + web_contents_(nullptr), + processed_page_ready_(false), + browser_context_(nullptr), + weak_factory_(this) {} + +HeadlessShell::~HeadlessShell() {} + +void HeadlessShell::OnStart(HeadlessBrowser* browser) { + browser_ = browser; + + HeadlessBrowserContext::Builder context_builder = + browser_->CreateBrowserContextBuilder(); + // TODO(eseckler): These switches should also affect BrowserContexts that + // are created via DevTools later. + if (base::CommandLine::ForCurrentProcess()->HasSwitch( + switches::kDeterministicFetch)) { + deterministic_dispatcher_.reset( + new DeterministicDispatcher(browser_->BrowserIOThread())); + + ProtocolHandlerMap protocol_handlers; + protocol_handlers[url::kHttpScheme] = + base::MakeUnique<DeterministicHttpProtocolHandler>( + deterministic_dispatcher_.get(), browser->BrowserIOThread()); + protocol_handlers[url::kHttpsScheme] = + base::MakeUnique<DeterministicHttpProtocolHandler>( + deterministic_dispatcher_.get(), browser->BrowserIOThread()); + + context_builder.SetProtocolHandlers(std::move(protocol_handlers)); + } + browser_context_ = context_builder.Build(); + browser_->SetDefaultBrowserContext(browser_context_); + + HeadlessWebContents::Builder builder( + browser_context_->CreateWebContentsBuilder()); + base::CommandLine::StringVector args = + base::CommandLine::ForCurrentProcess()->GetArgs(); + + // TODO(alexclarke): Should we navigate to about:blank first if using + // virtual time? + if (args.empty()) +#if defined(OS_WIN) + args.push_back(L"about:blank"); +#else + args.push_back("about:blank"); +#endif + for (auto it = args.rbegin(); it != args.rend(); ++it) { + GURL url(*it); + HeadlessWebContents* web_contents = builder.SetInitialURL(url).Build(); + if (!web_contents) { + LOG(ERROR) << "Navigation to " << url << " failed"; + browser_->Shutdown(); return; - if (!RemoteDebuggingEnabled()) { - devtools_client_->GetEmulation()->GetExperimental()->RemoveObserver(this); - devtools_client_->GetInspector()->GetExperimental()->RemoveObserver(this); - devtools_client_->GetPage()->RemoveObserver(this); - if (web_contents_->GetDevToolsTarget()) { - web_contents_->GetDevToolsTarget()->DetachClient( - devtools_client_.get()); - } - } - web_contents_->RemoveObserver(this); - web_contents_ = nullptr; - browser_context_->Close(); - browser_->Shutdown(); - } - - // HeadlessWebContents::Observer implementation: - void DevToolsTargetReady() override { - web_contents_->GetDevToolsTarget()->AttachClient(devtools_client_.get()); - devtools_client_->GetInspector()->GetExperimental()->AddObserver(this); - devtools_client_->GetPage()->AddObserver(this); - devtools_client_->GetPage()->Enable(); - // Check if the document had already finished loading by the time we - // attached. - - devtools_client_->GetEmulation()->GetExperimental()->AddObserver(this); - - if (base::CommandLine::ForCurrentProcess()->HasSwitch( - switches::kVirtualTimeBudget)) { - std::string budget_ms_ascii = - base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( - switches::kVirtualTimeBudget); - int budget_ms; - CHECK(base::StringToInt(budget_ms_ascii, &budget_ms)) - << "Expected an integer value for --virtual-time-budget="; - devtools_client_->GetEmulation()->GetExperimental()->SetVirtualTimePolicy( - emulation::SetVirtualTimePolicyParams::Builder() - .SetPolicy(emulation::VirtualTimePolicy:: - PAUSE_IF_NETWORK_FETCHES_PENDING) - .SetBudget(budget_ms) - .Build()); - } else { - PollReadyState(); } - - if (base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kTimeout)) { - std::string timeout_ms_ascii = - base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( - switches::kTimeout); - int timeout_ms; - CHECK(base::StringToInt(timeout_ms_ascii, &timeout_ms)) - << "Expected an integer value for --timeout="; - browser_->BrowserMainThread()->PostDelayedTask( - FROM_HERE, - base::Bind(&HeadlessShell::FetchTimeout, weak_factory_.GetWeakPtr()), - base::TimeDelta::FromMilliseconds(timeout_ms)); + if (!web_contents_ && !RemoteDebuggingEnabled()) { + // TODO(jzfeng): Support observing multiple targets. + url_ = url; + web_contents_ = web_contents; + web_contents_->AddObserver(this); } - - // TODO(skyostil): Implement more features to demonstrate the devtools API. - } - - void FetchTimeout() { - LOG(INFO) << "Timeout."; - devtools_client_->GetPage()->GetExperimental()->StopLoading( - page::StopLoadingParams::Builder().Build()); - } - - void OnTargetCrashed(const inspector::TargetCrashedParams& params) override { - LOG(ERROR) << "Abnormal renderer termination."; - // NB this never gets called if remote debugging is enabled. - Shutdown(); } +} - void PollReadyState() { - // We need to check the current location in addition to the ready state to - // be sure the expected page is ready. - devtools_client_->GetRuntime()->Evaluate( - "document.readyState + ' ' + document.location.href", - base::Bind(&HeadlessShell::OnReadyState, weak_factory_.GetWeakPtr())); +void HeadlessShell::Shutdown() { + if (!web_contents_) + return; + if (!RemoteDebuggingEnabled()) { + devtools_client_->GetEmulation()->GetExperimental()->RemoveObserver(this); + devtools_client_->GetInspector()->GetExperimental()->RemoveObserver(this); + devtools_client_->GetPage()->GetExperimental()->RemoveObserver(this); + if (web_contents_->GetDevToolsTarget()) { + web_contents_->GetDevToolsTarget()->DetachClient(devtools_client_.get()); + } } + web_contents_->RemoveObserver(this); + web_contents_ = nullptr; + browser_context_->Close(); + browser_->Shutdown(); +} - void OnReadyState(std::unique_ptr<runtime::EvaluateResult> result) { - std::string ready_state_and_url; - if (result->GetResult()->GetValue()->GetAsString(&ready_state_and_url)) { - std::stringstream stream(ready_state_and_url); - std::string ready_state; - std::string url; - stream >> ready_state; - stream >> url; +void HeadlessShell::DevToolsTargetReady() { + web_contents_->GetDevToolsTarget()->AttachClient(devtools_client_.get()); + devtools_client_->GetInspector()->GetExperimental()->AddObserver(this); + devtools_client_->GetPage()->GetExperimental()->AddObserver(this); + devtools_client_->GetPage()->Enable(); + // Check if the document had already finished loading by the time we + // attached. + + devtools_client_->GetEmulation()->GetExperimental()->AddObserver(this); + + if (base::CommandLine::ForCurrentProcess()->HasSwitch( + switches::kDeterministicFetch)) { + devtools_client_->GetPage()->GetExperimental()->SetControlNavigations( + headless::page::SetControlNavigationsParams::Builder() + .SetEnabled(true) + .Build()); + } + + if (base::CommandLine::ForCurrentProcess()->HasSwitch( + switches::kVirtualTimeBudget)) { + std::string budget_ms_ascii = + base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( + switches::kVirtualTimeBudget); + int budget_ms; + CHECK(base::StringToInt(budget_ms_ascii, &budget_ms)) + << "Expected an integer value for --virtual-time-budget="; + devtools_client_->GetEmulation()->GetExperimental()->SetVirtualTimePolicy( + emulation::SetVirtualTimePolicyParams::Builder() + .SetPolicy( + emulation::VirtualTimePolicy::PAUSE_IF_NETWORK_FETCHES_PENDING) + .SetBudget(budget_ms) + .Build()); + } else { + PollReadyState(); + } + + if (base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kTimeout)) { + std::string timeout_ms_ascii = + base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( + switches::kTimeout); + int timeout_ms; + CHECK(base::StringToInt(timeout_ms_ascii, &timeout_ms)) + << "Expected an integer value for --timeout="; + browser_->BrowserMainThread()->PostDelayedTask( + FROM_HERE, + base::Bind(&HeadlessShell::FetchTimeout, weak_factory_.GetWeakPtr()), + base::TimeDelta::FromMilliseconds(timeout_ms)); + } + + // TODO(skyostil): Implement more features to demonstrate the devtools API. +} - if (ready_state == "complete" && - (url_.spec() == url || url != "about:blank")) { - OnPageReady(); - return; - } - } - } +void HeadlessShell::FetchTimeout() { + LOG(INFO) << "Timeout."; + devtools_client_->GetPage()->GetExperimental()->StopLoading( + page::StopLoadingParams::Builder().Build()); +} - // emulation::Observer implementation: - void OnVirtualTimeBudgetExpired( - const emulation::VirtualTimeBudgetExpiredParams& params) override { - OnPageReady(); - } +void HeadlessShell::OnTargetCrashed( + const inspector::TargetCrashedParams& params) { + LOG(ERROR) << "Abnormal renderer termination."; + // NB this never gets called if remote debugging is enabled. + Shutdown(); +} - // page::Observer implementation: - void OnLoadEventFired(const page::LoadEventFiredParams& params) override { - if (base::CommandLine::ForCurrentProcess()->HasSwitch( - switches::kVirtualTimeBudget)) { - return; - } - OnPageReady(); - } +void HeadlessShell::PollReadyState() { + // We need to check the current location in addition to the ready state to + // be sure the expected page is ready. + devtools_client_->GetRuntime()->Evaluate( + "document.readyState + ' ' + document.location.href", + base::Bind(&HeadlessShell::OnReadyState, weak_factory_.GetWeakPtr())); +} - void OnPageReady() { - if (processed_page_ready_) +void HeadlessShell::OnReadyState( + std::unique_ptr<runtime::EvaluateResult> result) { + std::string ready_state_and_url; + if (result->GetResult()->GetValue()->GetAsString(&ready_state_and_url)) { + std::stringstream stream(ready_state_and_url); + std::string ready_state; + std::string url; + stream >> ready_state; + stream >> url; + + if (ready_state == "complete" && + (url_.spec() == url || url != "about:blank")) { + OnPageReady(); return; - processed_page_ready_ = true; - - if (base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kDumpDom)) { - FetchDom(); - } else if (base::CommandLine::ForCurrentProcess()->HasSwitch( - switches::kRepl)) { - LOG(INFO) - << "Type a Javascript expression to evaluate or \"quit\" to exit."; - InputExpression(); - } else if (base::CommandLine::ForCurrentProcess()->HasSwitch( - switches::kScreenshot)) { - CaptureScreenshot(); - } else { - Shutdown(); } } +} - void FetchDom() { - devtools_client_->GetRuntime()->Evaluate( - "document.body.innerHTML", - base::Bind(&HeadlessShell::OnDomFetched, weak_factory_.GetWeakPtr())); - } +// emulation::Observer implementation: +void HeadlessShell::OnVirtualTimeBudgetExpired( + const emulation::VirtualTimeBudgetExpiredParams& params) { + OnPageReady(); +} - void OnDomFetched(std::unique_ptr<runtime::EvaluateResult> result) { - if (result->HasExceptionDetails()) { - LOG(ERROR) << "Failed to evaluate document.body.innerHTML: " - << result->GetExceptionDetails()->GetText(); - } else { - std::string dom; - if (result->GetResult()->GetValue()->GetAsString(&dom)) { - printf("%s\n", dom.c_str()); - } - } - Shutdown(); +// page::Observer implementation: +void HeadlessShell::OnLoadEventFired(const page::LoadEventFiredParams& params) { + if (base::CommandLine::ForCurrentProcess()->HasSwitch( + switches::kVirtualTimeBudget)) { + return; } + OnPageReady(); +} - void InputExpression() { - // Note that a real system should read user input asynchronously, because - // otherwise all other browser activity is suspended (e.g., page loading). - printf(">>> "); - std::stringstream expression; - while (true) { - int c = fgetc(stdin); - if (c == EOF || c == '\n') { - break; - } - expression << c; - } - if (expression.str() == "quit") { - Shutdown(); - return; - } - devtools_client_->GetRuntime()->Evaluate( - expression.str(), base::Bind(&HeadlessShell::OnExpressionResult, - weak_factory_.GetWeakPtr())); - } +void HeadlessShell::OnNavigationRequested( + const headless::page::NavigationRequestedParams& params) { + deterministic_dispatcher_->NavigationRequested( + base::MakeUnique<ShellNavigationRequest>(weak_factory_.GetWeakPtr(), + params)); +} - void OnExpressionResult(std::unique_ptr<runtime::EvaluateResult> result) { - std::unique_ptr<base::Value> value = result->Serialize(); - std::string result_json; - base::JSONWriter::Write(*value, &result_json); - printf("%s\n", result_json.c_str()); +void HeadlessShell::OnPageReady() { + if (processed_page_ready_) + return; + processed_page_ready_ = true; + + if (base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kDumpDom)) { + FetchDom(); + } else if (base::CommandLine::ForCurrentProcess()->HasSwitch( + switches::kRepl)) { + LOG(INFO) + << "Type a Javascript expression to evaluate or \"quit\" to exit."; InputExpression(); + } else if (base::CommandLine::ForCurrentProcess()->HasSwitch( + switches::kScreenshot)) { + CaptureScreenshot(); + } else { + Shutdown(); } +} - void CaptureScreenshot() { - devtools_client_->GetPage()->GetExperimental()->CaptureScreenshot( - page::CaptureScreenshotParams::Builder().Build(), - base::Bind(&HeadlessShell::OnScreenshotCaptured, - weak_factory_.GetWeakPtr())); - } +void HeadlessShell::FetchDom() { + devtools_client_->GetRuntime()->Evaluate( + "document.body.outerHTML", + base::Bind(&HeadlessShell::OnDomFetched, weak_factory_.GetWeakPtr())); +} - void OnScreenshotCaptured( - std::unique_ptr<page::CaptureScreenshotResult> result) { - base::FilePath file_name = - base::CommandLine::ForCurrentProcess()->GetSwitchValuePath( - switches::kScreenshot); - if (file_name.empty()) { - file_name = base::FilePath().AppendASCII(kDefaultScreenshotFileName); +void HeadlessShell::OnDomFetched( + std::unique_ptr<runtime::EvaluateResult> result) { + if (result->HasExceptionDetails()) { + LOG(ERROR) << "Failed to evaluate document.body.outerHTML: " + << result->GetExceptionDetails()->GetText(); + } else { + std::string dom; + if (result->GetResult()->GetValue()->GetAsString(&dom)) { + printf("%s\n", dom.c_str()); } + } + Shutdown(); +} - screenshot_file_stream_.reset( - new net::FileStream(browser_->BrowserFileThread())); - const int open_result = screenshot_file_stream_->Open( - file_name, base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE | - base::File::FLAG_ASYNC, - base::Bind(&HeadlessShell::OnScreenshotFileOpened, - weak_factory_.GetWeakPtr(), base::Passed(std::move(result)), - file_name)); - if (open_result != net::ERR_IO_PENDING) { - // Operation could not be started. - OnScreenshotFileOpened(nullptr, file_name, open_result); +void HeadlessShell::InputExpression() { + // Note that a real system should read user input asynchronously, because + // otherwise all other browser activity is suspended (e.g., page loading). + printf(">>> "); + std::stringstream expression; + while (true) { + int c = fgetc(stdin); + if (c == EOF || c == '\n') { + break; } + expression << static_cast<char>(c); } + if (expression.str() == "quit") { + Shutdown(); + return; + } + devtools_client_->GetRuntime()->Evaluate( + expression.str(), base::Bind(&HeadlessShell::OnExpressionResult, + weak_factory_.GetWeakPtr())); +} - void OnScreenshotFileOpened( - std::unique_ptr<page::CaptureScreenshotResult> result, - const base::FilePath file_name, - const int open_result) { - if (open_result != net::OK) { - LOG(ERROR) << "Writing screenshot to file " << file_name.value() - << " was unsuccessful, could not open file: " - << net::ErrorToString(open_result); - return; - } +void HeadlessShell::OnExpressionResult( + std::unique_ptr<runtime::EvaluateResult> result) { + std::unique_ptr<base::Value> value = result->Serialize(); + std::string result_json; + base::JSONWriter::Write(*value, &result_json); + printf("%s\n", result_json.c_str()); + InputExpression(); +} - std::string decoded_png; - base::Base64Decode(result->GetData(), &decoded_png); - scoped_refptr<net::IOBufferWithSize> buf = - new net::IOBufferWithSize(decoded_png.size()); - memcpy(buf->data(), decoded_png.data(), decoded_png.size()); - const int write_result = screenshot_file_stream_->Write( - buf.get(), buf->size(), - base::Bind(&HeadlessShell::OnScreenshotFileWritten, - weak_factory_.GetWeakPtr(), file_name, buf->size())); - if (write_result != net::ERR_IO_PENDING) { - // Operation may have completed successfully or failed. - OnScreenshotFileWritten(file_name, buf->size(), write_result); - } - } +void HeadlessShell::CaptureScreenshot() { + devtools_client_->GetPage()->GetExperimental()->CaptureScreenshot( + page::CaptureScreenshotParams::Builder().Build(), + base::Bind(&HeadlessShell::OnScreenshotCaptured, + weak_factory_.GetWeakPtr())); +} - void OnScreenshotFileWritten(const base::FilePath file_name, - const int length, - const int write_result) { - if (write_result < length) { - // TODO(eseckler): Support recovering from partial writes. - LOG(ERROR) << "Writing screenshot to file " << file_name.value() - << " was unsuccessful: " << net::ErrorToString(write_result); - } else { - LOG(INFO) << "Screenshot written to file " << file_name.value() << "." - << std::endl; - } - int close_result = screenshot_file_stream_->Close(base::Bind( - &HeadlessShell::OnScreenshotFileClosed, weak_factory_.GetWeakPtr())); - if (close_result != net::ERR_IO_PENDING) { - // Operation could not be started. - OnScreenshotFileClosed(close_result); - } +void HeadlessShell::OnScreenshotCaptured( + std::unique_ptr<page::CaptureScreenshotResult> result) { + base::FilePath file_name = + base::CommandLine::ForCurrentProcess()->GetSwitchValuePath( + switches::kScreenshot); + if (file_name.empty()) { + file_name = base::FilePath().AppendASCII(kDefaultScreenshotFileName); + } + + screenshot_file_proxy_.reset( + new base::FileProxy(browser_->BrowserFileThread().get())); + if (!screenshot_file_proxy_->CreateOrOpen( + file_name, base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE, + base::Bind(&HeadlessShell::OnScreenshotFileOpened, + weak_factory_.GetWeakPtr(), + base::Passed(std::move(result)), file_name))) { + // Operation could not be started. + OnScreenshotFileOpened(nullptr, file_name, base::File::FILE_ERROR_FAILED); } +} - void OnScreenshotFileClosed(const int close_result) { Shutdown(); } +void HeadlessShell::OnScreenshotFileOpened( + std::unique_ptr<page::CaptureScreenshotResult> result, + const base::FilePath file_name, + base::File::Error error_code) { + if (!screenshot_file_proxy_->IsValid()) { + LOG(ERROR) << "Writing screenshot to file " << file_name.value() + << " was unsuccessful, could not open file: " + << base::File::ErrorToString(error_code); + return; + } + + std::string decoded_png; + base::Base64Decode(result->GetData(), &decoded_png); + scoped_refptr<net::IOBufferWithSize> buf = + new net::IOBufferWithSize(decoded_png.size()); + memcpy(buf->data(), decoded_png.data(), decoded_png.size()); + + if (!screenshot_file_proxy_->Write( + 0, buf->data(), buf->size(), + base::Bind(&HeadlessShell::OnScreenshotFileWritten, + weak_factory_.GetWeakPtr(), file_name, buf->size()))) { + // Operation may have completed successfully or failed. + OnScreenshotFileWritten(file_name, buf->size(), + base::File::FILE_ERROR_FAILED, 0); + } +} - bool RemoteDebuggingEnabled() const { - const base::CommandLine& command_line = - *base::CommandLine::ForCurrentProcess(); - return command_line.HasSwitch(::switches::kRemoteDebuggingPort); +void HeadlessShell::OnScreenshotFileWritten(const base::FilePath file_name, + const int length, + base::File::Error error_code, + int write_result) { + if (write_result < length) { + // TODO(eseckler): Support recovering from partial writes. + LOG(ERROR) << "Writing screenshot to file " << file_name.value() + << " was unsuccessful: " << net::ErrorToString(write_result); + } else { + LOG(INFO) << "Screenshot written to file " << file_name.value() << "." + << std::endl; + } + if (!screenshot_file_proxy_->Close( + base::Bind(&HeadlessShell::OnScreenshotFileClosed, + weak_factory_.GetWeakPtr()))) { + // Operation could not be started. + OnScreenshotFileClosed(base::File::FILE_ERROR_FAILED); } +} - private: - GURL url_; - HeadlessBrowser* browser_; // Not owned. - std::unique_ptr<HeadlessDevToolsClient> devtools_client_; - HeadlessWebContents* web_contents_; - bool processed_page_ready_; - std::unique_ptr<net::FileStream> screenshot_file_stream_; - HeadlessBrowserContext* browser_context_; - std::unique_ptr<DeterministicDispatcher> deterministic_dispatcher_; - base::WeakPtrFactory<HeadlessShell> weak_factory_; +void HeadlessShell::OnScreenshotFileClosed(base::File::Error error_code) { + Shutdown(); +} - DISALLOW_COPY_AND_ASSIGN(HeadlessShell); -}; +bool HeadlessShell::RemoteDebuggingEnabled() const { + const base::CommandLine& command_line = + *base::CommandLine::ForCurrentProcess(); + return command_line.HasSwitch(switches::kRemoteDebuggingPort); +} bool ValidateCommandLine(const base::CommandLine& command_line) { - if (!command_line.HasSwitch(::switches::kRemoteDebuggingPort)) { + if (!command_line.HasSwitch(switches::kRemoteDebuggingPort)) { if (command_line.GetArgs().size() <= 1) return true; LOG(ERROR) << "Open multiple tabs is only supported when the " @@ -444,15 +431,24 @@ bool ValidateCommandLine(const base::CommandLine& command_line) { } int HeadlessShellMain(int argc, const char** argv) { + base::CommandLine::Init(argc, argv); RunChildProcessIfNeeded(argc, argv); HeadlessShell shell; HeadlessBrowser::Options::Builder builder(argc, argv); // Enable devtools if requested. - base::CommandLine command_line(argc, argv); + const base::CommandLine& command_line( + *base::CommandLine::ForCurrentProcess()); if (!ValidateCommandLine(command_line)) return EXIT_FAILURE; + if (command_line.HasSwitch(::switches::kEnableCrashReporter)) + builder.SetCrashReporterEnabled(true); + if (command_line.HasSwitch(switches::kCrashDumpsDir)) { + builder.SetCrashDumpsDir( + command_line.GetSwitchValuePath(switches::kCrashDumpsDir)); + } + if (command_line.HasSwitch(::switches::kRemoteDebuggingPort)) { std::string address = kDevToolsHttpServerAddress; if (command_line.HasSwitch(switches::kRemoteDebuggingAddress)) { @@ -491,9 +487,9 @@ int HeadlessShellMain(int argc, const char** argv) { builder.SetProxyServer(parsed_proxy_server); } - if (command_line.HasSwitch(::switches::kHostResolverRules)) { + if (command_line.HasSwitch(switches::kHostResolverRules)) { builder.SetHostResolverRules( - command_line.GetSwitchValueASCII(::switches::kHostResolverRules)); + command_line.GetSwitchValueASCII(switches::kHostResolverRules)); } if (command_line.HasSwitch(switches::kUseGL)) { @@ -518,6 +514,11 @@ int HeadlessShellMain(int argc, const char** argv) { builder.SetWindowSize(parsed_window_size); } + if (command_line.HasSwitch(switches::kHideScrollbars)) { + builder.SetOverrideWebPreferencesCallback(base::Bind([]( + WebPreferences* preferences) { preferences->hide_scrollbars = true; })); + } + return HeadlessBrowserMain( builder.Build(), base::Bind(&HeadlessShell::OnStart, base::Unretained(&shell))); diff --git a/chromium/headless/app/headless_shell.h b/chromium/headless/app/headless_shell.h new file mode 100644 index 00000000000..4b0e83f6cbf --- /dev/null +++ b/chromium/headless/app/headless_shell.h @@ -0,0 +1,98 @@ +// Copyright 2017 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 "base/files/file_proxy.h" +#include "base/memory/weak_ptr.h" +#include "headless/app/shell_navigation_request.h" +#include "headless/public/devtools/domains/emulation.h" +#include "headless/public/devtools/domains/inspector.h" +#include "headless/public/devtools/domains/page.h" +#include "headless/public/devtools/domains/runtime.h" +#include "headless/public/headless_browser.h" +#include "headless/public/headless_devtools_client.h" +#include "headless/public/headless_web_contents.h" +#include "headless/public/util/deterministic_dispatcher.h" +#include "net/base/file_stream.h" + +namespace headless { + +// An application which implements a simple headless browser. +class HeadlessShell : public HeadlessWebContents::Observer, + public emulation::ExperimentalObserver, + public inspector::ExperimentalObserver, + public page::ExperimentalObserver { + public: + HeadlessShell(); + ~HeadlessShell() override; + + // HeadlessWebContents::Observer implementation: + void DevToolsTargetReady() override; + void OnTargetCrashed(const inspector::TargetCrashedParams& params) override; + + // emulation::Observer implementation: + void OnVirtualTimeBudgetExpired( + const emulation::VirtualTimeBudgetExpiredParams& params) override; + + // page::Observer implementation: + void OnLoadEventFired(const page::LoadEventFiredParams& params) override; + void OnNavigationRequested( + const headless::page::NavigationRequestedParams& params) override; + + void OnStart(HeadlessBrowser* browser); + void Shutdown(); + + void FetchTimeout(); + + void PollReadyState(); + + void OnReadyState(std::unique_ptr<runtime::EvaluateResult> result); + + void OnPageReady(); + + void FetchDom(); + + void OnDomFetched(std::unique_ptr<runtime::EvaluateResult> result); + + void InputExpression(); + + void OnExpressionResult(std::unique_ptr<runtime::EvaluateResult> result); + + void CaptureScreenshot(); + + void OnScreenshotCaptured( + std::unique_ptr<page::CaptureScreenshotResult> result); + + void OnScreenshotFileOpened( + std::unique_ptr<page::CaptureScreenshotResult> result, + const base::FilePath file_name, + base::File::Error error_code); + + void OnScreenshotFileWritten(const base::FilePath file_name, + const int length, + base::File::Error error_code, + int write_result); + + void OnScreenshotFileClosed(base::File::Error error_code); + + bool RemoteDebuggingEnabled() const; + + HeadlessDevToolsClient* devtools_client() const { + return devtools_client_.get(); + } + + private: + GURL url_; + HeadlessBrowser* browser_; // Not owned. + std::unique_ptr<HeadlessDevToolsClient> devtools_client_; + HeadlessWebContents* web_contents_; + bool processed_page_ready_; + std::unique_ptr<base::FileProxy> screenshot_file_proxy_; + HeadlessBrowserContext* browser_context_; + std::unique_ptr<DeterministicDispatcher> deterministic_dispatcher_; + base::WeakPtrFactory<HeadlessShell> weak_factory_; + + DISALLOW_COPY_AND_ASSIGN(HeadlessShell); +}; + +} // namespace headless diff --git a/chromium/headless/app/headless_shell_switches.cc b/chromium/headless/app/headless_shell_switches.cc index 06750596296..f20a60657f7 100644 --- a/chromium/headless/app/headless_shell_switches.cc +++ b/chromium/headless/app/headless_shell_switches.cc @@ -7,6 +7,9 @@ namespace headless { namespace switches { +// The directory breakpad should store minidumps in. +const char kCrashDumpsDir[] = "crash-dumps-dir"; + // Instructs headless_shell to cause network fetches to complete in order of // creation. This removes a significant source of network related // non-determinism at the cost of slower page loads. diff --git a/chromium/headless/app/headless_shell_switches.h b/chromium/headless/app/headless_shell_switches.h index 9d0a348e6e7..929fda626ac 100644 --- a/chromium/headless/app/headless_shell_switches.h +++ b/chromium/headless/app/headless_shell_switches.h @@ -5,8 +5,11 @@ #ifndef HEADLESS_APP_HEADLESS_SHELL_SWITCHES_H_ #define HEADLESS_APP_HEADLESS_SHELL_SWITCHES_H_ +#include "content/public/common/content_switches.h" + namespace headless { namespace switches { +extern const char kCrashDumpsDir[]; extern const char kDeterministicFetch[]; extern const char kDumpDom[]; extern const char kHideScrollbars[]; @@ -19,6 +22,11 @@ extern const char kUseGL[]; extern const char kUserDataDir[]; extern const char kVirtualTimeBudget[]; extern const char kWindowSize[]; + +// Switches which are replicated from content. +using ::switches::kHostResolverRules; +using ::switches::kRemoteDebuggingPort; + } // namespace switches } // namespace headless diff --git a/chromium/headless/app/shell_navigation_request.cc b/chromium/headless/app/shell_navigation_request.cc new file mode 100644 index 00000000000..3c419bce8ec --- /dev/null +++ b/chromium/headless/app/shell_navigation_request.cc @@ -0,0 +1,43 @@ +// Copyright 2017 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 "headless/app/shell_navigation_request.h" + +#include "headless/app/headless_shell.h" + +namespace headless { + +ShellNavigationRequest::ShellNavigationRequest( + base::WeakPtr<HeadlessShell> headless_shell, + const page::NavigationRequestedParams& params) + : headless_shell_(headless_shell), + navigation_id_(params.GetNavigationId()) {} + +ShellNavigationRequest::~ShellNavigationRequest() {} + +void ShellNavigationRequest::StartProcessing(base::Closure done_callback) { + if (!headless_shell_) + return; + + // Allow the navigation to proceed. + headless_shell_->devtools_client() + ->GetPage() + ->GetExperimental() + ->ProcessNavigation( + headless::page::ProcessNavigationParams::Builder() + .SetNavigationId(navigation_id_) + .SetResponse(headless::page::NavigationResponse::PROCEED) + .Build(), + base::Bind(&ShellNavigationRequest::ProcessNavigationResult, + done_callback)); +} + +// static +void ShellNavigationRequest::ProcessNavigationResult( + base::Closure done_callback, + std::unique_ptr<page::ProcessNavigationResult>) { + done_callback.Run(); +} + +} // namespace headless diff --git a/chromium/headless/app/shell_navigation_request.h b/chromium/headless/app/shell_navigation_request.h new file mode 100644 index 00000000000..20ef77f0e4e --- /dev/null +++ b/chromium/headless/app/shell_navigation_request.h @@ -0,0 +1,41 @@ +// Copyright 2017 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. + +#ifndef HEADLESS_PUBLIC_UTIL_HTTP_URL_FETCHER_H_ +#define HEADLESS_PUBLIC_UTIL_HTTP_URL_FETCHER_H_ + +#include "base/callback.h" +#include "base/macros.h" +#include "base/memory/weak_ptr.h" +#include "headless/public/devtools/domains/page.h" +#include "headless/public/util/navigation_request.h" + +namespace headless { +class HeadlessShell; + +// Used in deterministic mode to make sure navigations and resource requests +// complete in the order requested. +class ShellNavigationRequest : public NavigationRequest { + public: + ShellNavigationRequest(base::WeakPtr<HeadlessShell> headless_shell, + const page::NavigationRequestedParams& params); + + ~ShellNavigationRequest() override; + + void StartProcessing(base::Closure done_callback) override; + + private: + // Note the navigation likely isn't done when this is called, however we + // expect it will have been committed and the initial resource load requested. + static void ProcessNavigationResult( + base::Closure done_callback, + std::unique_ptr<page::ProcessNavigationResult>); + + base::WeakPtr<HeadlessShell> headless_shell_; + int navigation_id_; +}; + +} // namespace headless + +#endif // HEADLESS_PUBLIC_UTIL_HTTP_URL_FETCHER_H_ diff --git a/chromium/headless/headless.gni b/chromium/headless/headless.gni new file mode 100644 index 00000000000..0cc7722927c --- /dev/null +++ b/chromium/headless/headless.gni @@ -0,0 +1,8 @@ +# Copyright 2017 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. + +declare_args() { + # Embed resource.pak file into the binary for easier distribution. + headless_use_embedded_resources = false +} diff --git a/chromium/headless/lib/browser/DEPS b/chromium/headless/lib/browser/DEPS index 1704e22e758..0df32e1eda5 100644 --- a/chromium/headless/lib/browser/DEPS +++ b/chromium/headless/lib/browser/DEPS @@ -1,4 +1,5 @@ include_rules = [ "+components/security_state", + "+storage/browser/quota", "+ui/aura", ] diff --git a/chromium/headless/lib/browser/devtools_api/client_api_generator.py b/chromium/headless/lib/browser/devtools_api/client_api_generator.py index 435358af73d..fd011e10ffd 100644 --- a/chromium/headless/lib/browser/devtools_api/client_api_generator.py +++ b/chromium/headless/lib/browser/devtools_api/client_api_generator.py @@ -65,6 +65,11 @@ def SanitizeLiteral(literal): return { # Rename null enumeration values to avoid a clash with the NULL macro. 'null': 'none', + # Rename literals that clash with Win32 defined macros. + 'error': 'err', + 'mouseMoved': 'mouse_ptr_moved', + 'Strict': 'exact', + 'getCurrentTime': 'getCurrentAnimationTime', # Rename mathematical constants to avoid colliding with C macros. 'Infinity': 'InfinityValue', '-Infinity': 'NegativeInfinityValue', @@ -321,10 +326,10 @@ def SynthesizeCommandTypes(json_api): if 'enum' in parameter and not '$ref' in parameter: SynthesizeEnumType(domain, command['name'], parameter) parameters_type = { - 'id': ToTitleCase(command['name']) + 'Params', + 'id': ToTitleCase(SanitizeLiteral(command['name'])) + 'Params', 'type': 'object', 'description': 'Parameters for the %s command.' % ToTitleCase( - command['name']), + SanitizeLiteral(command['name'])), 'properties': command['parameters'] } domain['types'].append(parameters_type) @@ -333,10 +338,10 @@ def SynthesizeCommandTypes(json_api): if 'enum' in parameter and not '$ref' in parameter: SynthesizeEnumType(domain, command['name'], parameter) result_type = { - 'id': ToTitleCase(command['name']) + 'Result', + 'id': ToTitleCase(SanitizeLiteral(command['name'])) + 'Result', 'type': 'object', 'description': 'Result for the %s command.' % ToTitleCase( - command['name']), + SanitizeLiteral(command['name'])), 'properties': command['returns'] } domain['types'].append(result_type) diff --git a/chromium/headless/lib/browser/devtools_api/domain_cc.template b/chromium/headless/lib/browser/devtools_api/domain_cc.template index dc04d6e9d7b..f9b70377917 100644 --- a/chromium/headless/lib/browser/devtools_api/domain_cc.template +++ b/chromium/headless/lib/browser/devtools_api/domain_cc.template @@ -45,7 +45,7 @@ void Domain::RegisterEventHandlersIfNeeded() { {% if "redirect" in command %}{% continue %}{% endif %} {% set class_name = 'ExperimentalDomain' if command.experimental else 'Domain' %} - {% set method_name = command.name | to_title_case %} + {% set method_name = command.name | sanitize_literal | to_title_case %} void {{class_name}}::{{method_name}}(std::unique_ptr<{{method_name}}Params> params, base::Callback<void(std::unique_ptr<{{method_name}}Result>)> callback) { dispatcher_->SendMessage("{{domain.domain}}.{{command.name}}", params->Serialize(), base::Bind(&Domain::Handle{{method_name}}Response, callback)); } @@ -96,7 +96,7 @@ void {{class_name}}::{{method_name}}(std::unique_ptr<{{method_name}}Params> para {# Generate response handlers for commands that need them. #} {% for command in domain.commands %} {% if not "returns" in command %}{% continue %}{% endif %} - {% set method_name = command.name | to_title_case %} + {% set method_name = command.name | sanitize_literal | to_title_case %} // static void Domain::Handle{{method_name}}Response(base::Callback<void(std::unique_ptr<{{method_name}}Result>)> callback, const base::Value& response) { diff --git a/chromium/headless/lib/browser/devtools_api/domain_h.template b/chromium/headless/lib/browser/devtools_api/domain_h.template index 73110412f43..50073c08a35 100644 --- a/chromium/headless/lib/browser/devtools_api/domain_h.template +++ b/chromium/headless/lib/browser/devtools_api/domain_h.template @@ -18,7 +18,7 @@ {# Macro for defining a member function for a given command. #} {% macro command_decl(command) %} - {% set method_name = command.name | to_title_case %} + {% set method_name = command.name | sanitize_literal | to_title_case %} {% if command.description %} // {{ command.description }} {% endif %} @@ -108,7 +108,7 @@ class HEADLESS_EXPORT Domain { {# Generate response handlers for commands that need them. #} {% for command in domain.commands %} {% if not "returns" in command %}{% continue %}{% endif %} - {% set method_name = command.name | to_title_case %} + {% set method_name = command.name | sanitize_literal | to_title_case %} static void Handle{{method_name}}Response(base::Callback<void(std::unique_ptr<{{method_name}}Result>)> callback, const base::Value& response); {% endfor %} diff --git a/chromium/headless/lib/browser/headless_browser_context_impl.cc b/chromium/headless/lib/browser/headless_browser_context_impl.cc index df65523f34b..29c9da3f747 100644 --- a/chromium/headless/lib/browser/headless_browser_context_impl.cc +++ b/chromium/headless/lib/browser/headless_browser_context_impl.cc @@ -21,7 +21,6 @@ #include "headless/public/util/black_hole_protocol_handler.h" #include "headless/public/util/in_memory_protocol_handler.h" #include "net/url_request/url_request_context.h" -#include "ui/aura/window_tree_host.h" namespace headless { @@ -249,8 +248,7 @@ HeadlessWebContents* HeadlessBrowserContextImpl::CreateWebContents( DCHECK_CURRENTLY_ON(content::BrowserThread::UI); std::unique_ptr<HeadlessWebContentsImpl> headless_web_contents = - HeadlessWebContentsImpl::Create(builder, - browser()->window_tree_host()->window()); + HeadlessWebContentsImpl::Create(builder); if (!headless_web_contents) { return nullptr; @@ -322,6 +320,13 @@ HeadlessBrowserContext::Builder& HeadlessBrowserContext::Builder::SetUserAgent( } HeadlessBrowserContext::Builder& +HeadlessBrowserContext::Builder::SetProductNameAndVersion( + const std::string& product_name_and_version) { + options_->product_name_and_version_ = product_name_and_version; + return *this; +} + +HeadlessBrowserContext::Builder& HeadlessBrowserContext::Builder::SetProxyServer( const net::HostPortPair& proxy_server) { options_->proxy_server_ = proxy_server; diff --git a/chromium/headless/lib/browser/headless_browser_context_options.cc b/chromium/headless/lib/browser/headless_browser_context_options.cc index 13faa83889e..f9fa8248e5b 100644 --- a/chromium/headless/lib/browser/headless_browser_context_options.cc +++ b/chromium/headless/lib/browser/headless_browser_context_options.cc @@ -35,6 +35,12 @@ HeadlessBrowserContextOptions::HeadlessBrowserContextOptions( HeadlessBrowser::Options* options) : browser_options_(options) {} +const std::string& HeadlessBrowserContextOptions::product_name_and_version() + const { + return ReturnOverriddenValue(product_name_and_version_, + browser_options_->product_name_and_version); +} + const std::string& HeadlessBrowserContextOptions::user_agent() const { return ReturnOverriddenValue(user_agent_, browser_options_->user_agent); } @@ -61,6 +67,13 @@ bool HeadlessBrowserContextOptions::incognito_mode() const { browser_options_->incognito_mode); } +const base::Callback<void(WebPreferences*)>& +HeadlessBrowserContextOptions::override_web_preferences_callback() const { + return ReturnOverriddenValue( + override_web_preferences_callback_, + browser_options_->override_web_preferences_callback); +} + const ProtocolHandlerMap& HeadlessBrowserContextOptions::protocol_handlers() const { return protocol_handlers_; @@ -70,9 +83,4 @@ ProtocolHandlerMap HeadlessBrowserContextOptions::TakeProtocolHandlers() { return std::move(protocol_handlers_); } -const base::Callback<void(WebPreferences*)>& -HeadlessBrowserContextOptions::override_web_preferences_callback() const { - return override_web_preferences_callback_; -} - } // namespace headless diff --git a/chromium/headless/lib/browser/headless_browser_context_options.h b/chromium/headless/lib/browser/headless_browser_context_options.h index 4c6601757f4..61f2b23d83c 100644 --- a/chromium/headless/lib/browser/headless_browser_context_options.h +++ b/chromium/headless/lib/browser/headless_browser_context_options.h @@ -26,6 +26,7 @@ class HeadlessBrowserContextOptions { HeadlessBrowserContextOptions& operator=( HeadlessBrowserContextOptions&& options); + const std::string& product_name_and_version() const; const std::string& user_agent() const; // See HeadlessBrowser::Options::proxy_server. @@ -60,15 +61,17 @@ class HeadlessBrowserContextOptions { HeadlessBrowser::Options* browser_options_; + base::Optional<std::string> product_name_and_version_; base::Optional<std::string> user_agent_; base::Optional<net::HostPortPair> proxy_server_; base::Optional<std::string> host_resolver_rules_; base::Optional<gfx::Size> window_size_; base::Optional<base::FilePath> user_data_dir_; base::Optional<bool> incognito_mode_; + base::Optional<base::Callback<void(WebPreferences*)>> + override_web_preferences_callback_; ProtocolHandlerMap protocol_handlers_; - base::Callback<void(WebPreferences*)> override_web_preferences_callback_; DISALLOW_COPY_AND_ASSIGN(HeadlessBrowserContextOptions); }; diff --git a/chromium/headless/lib/browser/headless_browser_impl.cc b/chromium/headless/lib/browser/headless_browser_impl.cc index 93d6918a76c..71b79a09190 100644 --- a/chromium/headless/lib/browser/headless_browser_impl.cc +++ b/chromium/headless/lib/browser/headless_browser_impl.cc @@ -18,9 +18,8 @@ #include "headless/lib/browser/headless_browser_context_impl.h" #include "headless/lib/browser/headless_browser_main_parts.h" #include "headless/lib/browser/headless_web_contents_impl.h" -#include "headless/lib/browser/headless_window_parenting_client.h" -#include "headless/lib/browser/headless_window_tree_host.h" #include "headless/lib/headless_content_main_delegate.h" +#include "ui/aura/client/focus_client.h" #include "ui/aura/env.h" #include "ui/aura/window.h" #include "ui/events/devices/device_data_manager.h" @@ -121,17 +120,7 @@ void HeadlessBrowserImpl::set_browser_main_parts( } void HeadlessBrowserImpl::RunOnStartCallback() { - DCHECK(aura::Env::GetInstance()); - ui::DeviceDataManager::CreateInstance(); - - window_tree_host_.reset( - new HeadlessWindowTreeHost(gfx::Rect(options()->window_size))); - window_tree_host_->InitHost(); - window_tree_host_->window()->Show(); - - window_parenting_client_.reset( - new HeadlessWindowParentingClient(window_tree_host_->window())); - + PlatformCreateWindow(); on_start_callback_.Run(this); on_start_callback_ = base::Callback<void(HeadlessBrowser*)>(); } @@ -179,10 +168,6 @@ base::WeakPtr<HeadlessBrowserImpl> HeadlessBrowserImpl::GetWeakPtr() { return weak_ptr_factory_.GetWeakPtr(); } -aura::WindowTreeHost* HeadlessBrowserImpl::window_tree_host() const { - return window_tree_host_.get(); -} - HeadlessWebContents* HeadlessBrowserImpl::GetWebContentsForDevToolsAgentHostId( const std::string& devtools_agent_host_id) { for (HeadlessBrowserContext* context : GetAllBrowserContexts()) { @@ -203,8 +188,9 @@ HeadlessBrowserContext* HeadlessBrowserImpl::GetBrowserContextForId( } void RunChildProcessIfNeeded(int argc, const char** argv) { - base::CommandLine command_line(argc, argv); - if (!command_line.HasSwitch(switches::kProcessType)) + base::CommandLine::Init(argc, argv); + if (!base::CommandLine::ForCurrentProcess()->HasSwitch( + switches::kProcessType)) return; HeadlessBrowser::Options::Builder builder(argc, argv); @@ -223,8 +209,8 @@ int HeadlessBrowserMain( browser_was_initialized = true; // Child processes should not end up here. - base::CommandLine command_line(options.argc, options.argv); - DCHECK(!command_line.HasSwitch(switches::kProcessType)); + DCHECK(!base::CommandLine::ForCurrentProcess()->HasSwitch( + switches::kProcessType)); #endif return RunContentMain(std::move(options), std::move(on_browser_start_callback)); diff --git a/chromium/headless/lib/browser/headless_browser_impl.h b/chromium/headless/lib/browser/headless_browser_impl.h index f37dff1dbc4..864e49c1c17 100644 --- a/chromium/headless/lib/browser/headless_browser_impl.h +++ b/chromium/headless/lib/browser/headless_browser_impl.h @@ -13,16 +13,19 @@ #include <vector> #include "base/memory/weak_ptr.h" +#include "content/public/browser/web_contents.h" #include "headless/lib/browser/headless_devtools_manager_delegate.h" #include "headless/lib/browser/headless_web_contents_impl.h" -namespace aura { -class WindowTreeHost; +#if defined(USE_AURA) +#include "headless/lib/browser/headless_window_tree_host.h" +namespace aura { namespace client { -class WindowParentingClient; +class FocusClient; } } +#endif namespace headless { @@ -70,18 +73,26 @@ class HeadlessBrowserImpl : public HeadlessBrowser { base::WeakPtr<HeadlessBrowserImpl> GetWeakPtr(); - aura::WindowTreeHost* window_tree_host() const; + // All the methods that begin with Platform need to be implemented by the + // platform specific headless implementation. + // Helper for one time initialization of application + void PlatformInitialize(); + void PlatformCreateWindow(); + void PlatformInitializeWebContents(const gfx::Size& initial_size, + content::WebContents* web_contents); protected: - base::Callback<void(HeadlessBrowser*)> on_start_callback_; - HeadlessBrowser::Options options_; - HeadlessBrowserMainParts* browser_main_parts_; // Not owned. - +#if defined(USE_AURA) // TODO(eseckler): Currently one window and one window_tree_host // is used for all web contents. We should probably use one // window per web contents, but additional investigation is needed. - std::unique_ptr<aura::WindowTreeHost> window_tree_host_; - std::unique_ptr<aura::client::WindowParentingClient> window_parenting_client_; + std::unique_ptr<HeadlessWindowTreeHost> window_tree_host_; + std::unique_ptr<aura::client::FocusClient> focus_client_; +#endif + + base::Callback<void(HeadlessBrowser*)> on_start_callback_; + HeadlessBrowser::Options options_; + HeadlessBrowserMainParts* browser_main_parts_; // Not owned. std::unordered_map<std::string, std::unique_ptr<HeadlessBrowserContextImpl>> browser_contexts_; diff --git a/chromium/headless/lib/browser/headless_browser_impl_aura.cc b/chromium/headless/lib/browser/headless_browser_impl_aura.cc new file mode 100644 index 00000000000..4718d4914dc --- /dev/null +++ b/chromium/headless/lib/browser/headless_browser_impl_aura.cc @@ -0,0 +1,56 @@ +// Copyright 2017 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 "headless/lib/browser/headless_browser_impl.h" + +#include "content/public/browser/render_widget_host_view.h" +#include "content/public/browser/web_contents.h" +#include "headless/lib/browser/headless_focus_client.h" +#include "headless/lib/browser/headless_screen.h" +#include "ui/aura/client/focus_client.h" +#include "ui/aura/env.h" +#include "ui/aura/window.h" +#include "ui/display/screen.h" +#include "ui/events/devices/device_data_manager.h" +#include "ui/gfx/geometry/size.h" + +namespace headless { + +void HeadlessBrowserImpl::PlatformInitialize() { + HeadlessScreen* screen = HeadlessScreen::Create(options()->window_size); + display::Screen::SetScreenInstance(screen); +} + +void HeadlessBrowserImpl::PlatformCreateWindow() { + DCHECK(aura::Env::GetInstance()); + ui::DeviceDataManager::CreateInstance(); + + window_tree_host_.reset( + new HeadlessWindowTreeHost(gfx::Rect(options()->window_size))); + window_tree_host_->InitHost(); + window_tree_host_->window()->Show(); + window_tree_host_->SetParentWindow(window_tree_host_->window()); + + focus_client_.reset(new HeadlessFocusClient()); + aura::client::SetFocusClient(window_tree_host_->window(), + focus_client_.get()); +} + +void HeadlessBrowserImpl::PlatformInitializeWebContents( + const gfx::Size& initial_size, + content::WebContents* web_contents) { + gfx::NativeView contents = web_contents->GetNativeView(); + gfx::NativeWindow parent_window = window_tree_host_->window(); + DCHECK(!parent_window->Contains(contents)); + parent_window->AddChild(contents); + contents->Show(); + contents->SetBounds(gfx::Rect(initial_size)); + + content::RenderWidgetHostView* host_view = + web_contents->GetRenderWidgetHostView(); + if (host_view) + host_view->SetSize(initial_size); +} + +} // namespace headless diff --git a/chromium/headless/lib/browser/headless_browser_impl_mac.mm b/chromium/headless/lib/browser/headless_browser_impl_mac.mm new file mode 100644 index 00000000000..e1911162d6e --- /dev/null +++ b/chromium/headless/lib/browser/headless_browser_impl_mac.mm @@ -0,0 +1,27 @@ +// Copyright 2017 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 "headless/lib/browser/headless_browser_impl.h" + +#include "content/public/browser/web_contents.h" +#include "ui/base/cocoa/base_view.h" + +namespace headless { + +void HeadlessBrowserImpl::PlatformInitialize() {} + +void HeadlessBrowserImpl::PlatformCreateWindow() {} + +void HeadlessBrowserImpl::PlatformInitializeWebContents( + const gfx::Size& initial_size, + content::WebContents* web_contents) { + NSView* web_view = web_contents->GetNativeView(); + [web_view setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)]; + + NSRect frame = NSMakeRect(0, 0, initial_size.width(), initial_size.height()); + [web_view setFrame:frame]; + return; +} + +} // namespace headless diff --git a/chromium/headless/lib/browser/headless_browser_main_parts.cc b/chromium/headless/lib/browser/headless_browser_main_parts.cc index b219480b6f7..2ae52c3cc31 100644 --- a/chromium/headless/lib/browser/headless_browser_main_parts.cc +++ b/chromium/headless/lib/browser/headless_browser_main_parts.cc @@ -8,23 +8,9 @@ #include "headless/lib/browser/headless_browser_impl.h" #include "headless/lib/browser/headless_devtools.h" #include "headless/lib/browser/headless_screen.h" -#include "ui/aura/env.h" -#include "ui/display/screen.h" namespace headless { -namespace { - -void PlatformInitialize(const gfx::Size& screen_size) { - HeadlessScreen* screen = HeadlessScreen::Create(screen_size); - display::Screen::SetScreenInstance(screen); -} - -void PlatformExit() { -} - -} // namespace - HeadlessBrowserMainParts::HeadlessBrowserMainParts(HeadlessBrowserImpl* browser) : browser_(browser) , devtools_http_handler_started_(false) {} @@ -36,7 +22,7 @@ void HeadlessBrowserMainParts::PreMainMessageLoopRun() { StartLocalDevToolsHttpHandler(browser_->options()); devtools_http_handler_started_ = true; } - PlatformInitialize(browser_->options()->window_size); + browser_->PlatformInitialize(); } void HeadlessBrowserMainParts::PostMainMessageLoopRun() { @@ -44,7 +30,6 @@ void HeadlessBrowserMainParts::PostMainMessageLoopRun() { StopLocalDevToolsHttpHandler(); devtools_http_handler_started_ = false; } - PlatformExit(); } } // namespace headless diff --git a/chromium/headless/lib/browser/headless_content_browser_client.cc b/chromium/headless/lib/browser/headless_content_browser_client.cc index 8ba656c082c..5876199424b 100644 --- a/chromium/headless/lib/browser/headless_content_browser_client.cc +++ b/chromium/headless/lib/browser/headless_content_browser_client.cc @@ -7,26 +7,100 @@ #include <memory> #include <unordered_set> +#include "base/base_switches.h" #include "base/callback.h" +#include "base/command_line.h" #include "base/json/json_reader.h" #include "base/memory/ptr_util.h" +#include "base/path_service.h" #include "content/public/browser/browser_context.h" #include "content/public/browser/browser_thread.h" #include "content/public/browser/render_process_host.h" #include "content/public/browser/render_view_host.h" +#include "content/public/browser/storage_partition.h" +#include "content/public/common/content_switches.h" #include "content/public/common/service_names.mojom.h" #include "headless/grit/headless_lib_resources.h" #include "headless/lib/browser/headless_browser_context_impl.h" #include "headless/lib/browser/headless_browser_impl.h" #include "headless/lib/browser/headless_browser_main_parts.h" #include "headless/lib/browser/headless_devtools_manager_delegate.h" +#include "headless/lib/headless_macros.h" +#include "storage/browser/quota/quota_settings.h" #include "ui/base/resource/resource_bundle.h" +#include "ui/gfx/switches.h" + +#if defined(HEADLESS_USE_BREAKPAD) +#include "base/debug/leak_annotations.h" +#include "components/crash/content/app/breakpad_linux.h" +#include "components/crash/content/browser/crash_handler_host_linux.h" +#include "content/public/common/content_descriptors.h" +#endif // defined(HEADLESS_USE_BREAKPAD) namespace headless { namespace { const char kCapabilityPath[] = "interface_provider_specs.navigation:frame.provides.renderer"; + +#if defined(HEADLESS_USE_BREAKPAD) +breakpad::CrashHandlerHostLinux* CreateCrashHandlerHost( + const std::string& process_type, + const HeadlessBrowser::Options& options) { + base::FilePath dumps_path = options.crash_dumps_dir; + if (dumps_path.empty()) { + bool ok = PathService::Get(base::DIR_MODULE, &dumps_path); + DCHECK(ok); + } + + { + ANNOTATE_SCOPED_MEMORY_LEAK; +#if defined(OFFICIAL_BUILD) + // Upload crash dumps in official builds, unless we're running in unattended + // mode (not to be confused with headless mode in general -- see + // chrome/common/env_vars.cc). + static const char kHeadless[] = "CHROME_HEADLESS"; + bool upload = (getenv(kHeadless) == nullptr); +#else + bool upload = false; +#endif + breakpad::CrashHandlerHostLinux* crash_handler = + new breakpad::CrashHandlerHostLinux(process_type, dumps_path, upload); + crash_handler->StartUploaderThread(); + return crash_handler; + } +} + +int GetCrashSignalFD(const base::CommandLine& command_line, + const HeadlessBrowser::Options& options) { + if (!breakpad::IsCrashReporterEnabled()) + return -1; + + std::string process_type = + command_line.GetSwitchValueASCII(switches::kProcessType); + + if (process_type == switches::kRendererProcess) { + static breakpad::CrashHandlerHostLinux* crash_handler = + CreateCrashHandlerHost(process_type, options); + return crash_handler->GetDeathSignalSocket(); + } + + if (process_type == switches::kPpapiPluginProcess) { + static breakpad::CrashHandlerHostLinux* crash_handler = + CreateCrashHandlerHost(process_type, options); + return crash_handler->GetDeathSignalSocket(); + } + + if (process_type == switches::kGpuProcess) { + static breakpad::CrashHandlerHostLinux* crash_handler = + CreateCrashHandlerHost(process_type, options); + return crash_handler->GetDeathSignalSocket(); + } + + return -1; +} +#endif // defined(HEADLESS_USE_BREAKPAD) + } // namespace HeadlessContentBrowserClient::HeadlessContentBrowserClient( @@ -46,7 +120,7 @@ content::BrowserMainParts* HeadlessContentBrowserClient::CreateBrowserMainParts( void HeadlessContentBrowserClient::OverrideWebkitPrefs( content::RenderViewHost* render_view_host, content::WebPreferences* prefs) { - auto browser_context = HeadlessBrowserContextImpl::From( + auto* browser_context = HeadlessBrowserContextImpl::From( render_view_host->GetProcess()->GetBrowserContext()); const base::Callback<void(headless::WebPreferences*)>& callback = browser_context->options()->override_web_preferences_callback(); @@ -86,4 +160,39 @@ HeadlessContentBrowserClient::GetServiceManifestOverlay( return manifest; } +void HeadlessContentBrowserClient::GetQuotaSettings( + content::BrowserContext* context, + content::StoragePartition* partition, + const storage::OptionalQuotaSettingsCallback& callback) { + content::BrowserThread::PostTaskAndReplyWithResult( + content::BrowserThread::FILE, FROM_HERE, + base::Bind(&storage::CalculateNominalDynamicSettings, + partition->GetPath(), context->IsOffTheRecord()), + callback); +} + +#if defined(OS_POSIX) && !defined(OS_MACOSX) +void HeadlessContentBrowserClient::GetAdditionalMappedFilesForChildProcess( + const base::CommandLine& command_line, + int child_process_id, + content::FileDescriptorInfo* mappings) { +#if defined(HEADLESS_USE_BREAKPAD) + int crash_signal_fd = GetCrashSignalFD(command_line, *browser_->options()); + if (crash_signal_fd >= 0) + mappings->Share(kCrashDumpSignal, crash_signal_fd); +#endif // defined(HEADLESS_USE_BREAKPAD) +} +#endif // defined(OS_POSIX) && !defined(OS_MACOSX) + +void HeadlessContentBrowserClient::AppendExtraCommandLineSwitches( + base::CommandLine* command_line, + int child_process_id) { + command_line->AppendSwitch(switches::kHeadless); +#if defined(HEADLESS_USE_BREAKPAD) + // This flag tells child processes to also turn on crash reporting. + if (breakpad::IsCrashReporterEnabled()) + command_line->AppendSwitch(switches::kEnableCrashReporter); +#endif // defined(HEADLESS_USE_BREAKPAD) +} + } // namespace headless diff --git a/chromium/headless/lib/browser/headless_content_browser_client.h b/chromium/headless/lib/browser/headless_content_browser_client.h index 3f443819867..53a950406e1 100644 --- a/chromium/headless/lib/browser/headless_content_browser_client.h +++ b/chromium/headless/lib/browser/headless_content_browser_client.h @@ -24,6 +24,18 @@ class HeadlessContentBrowserClient : public content::ContentBrowserClient { content::DevToolsManagerDelegate* GetDevToolsManagerDelegate() override; std::unique_ptr<base::Value> GetServiceManifestOverlay( base::StringPiece name) override; + void GetQuotaSettings( + content::BrowserContext* context, + content::StoragePartition* partition, + const storage::OptionalQuotaSettingsCallback& callback) override; +#if defined(OS_POSIX) && !defined(OS_MACOSX) + void GetAdditionalMappedFilesForChildProcess( + const base::CommandLine& command_line, + int child_process_id, + content::FileDescriptorInfo* mappings) override; +#endif + void AppendExtraCommandLineSwitches(base::CommandLine* command_line, + int child_process_id) override; private: HeadlessBrowserImpl* browser_; // Not owned. diff --git a/chromium/headless/lib/browser/headless_devtools.cc b/chromium/headless/lib/browser/headless_devtools.cc index ea97afcc96a..88b9656a763 100644 --- a/chromium/headless/lib/browser/headless_devtools.cc +++ b/chromium/headless/lib/browser/headless_devtools.cc @@ -56,15 +56,14 @@ class TCPServerSocketFactory : public content::DevToolsSocketFactory { } // namespace -void StartLocalDevToolsHttpHandler( - HeadlessBrowser::Options* options) { +void StartLocalDevToolsHttpHandler(HeadlessBrowser::Options* options) { const net::IPEndPoint& endpoint = options->devtools_endpoint; std::unique_ptr<content::DevToolsSocketFactory> socket_factory( new TCPServerSocketFactory(endpoint)); content::DevToolsAgentHost::StartRemoteDebuggingServer( std::move(socket_factory), std::string(), options->user_data_dir, // TODO(altimin): Figure a proper value for this. - base::FilePath(), std::string(), options->user_agent); + base::FilePath(), options->product_name_and_version, options->user_agent); } void StopLocalDevToolsHttpHandler() { diff --git a/chromium/headless/lib/browser/headless_focus_client.cc b/chromium/headless/lib/browser/headless_focus_client.cc new file mode 100644 index 00000000000..9a4e806e86c --- /dev/null +++ b/chromium/headless/lib/browser/headless_focus_client.cc @@ -0,0 +1,63 @@ +// Copyright 2017 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 "headless/lib/browser/headless_focus_client.h" + +#include "ui/aura/client/focus_change_observer.h" +#include "ui/aura/window.h" + +namespace headless { + +HeadlessFocusClient::HeadlessFocusClient() + : focused_window_(NULL), observer_manager_(this) {} + +HeadlessFocusClient::~HeadlessFocusClient() {} + +void HeadlessFocusClient::AddObserver( + aura::client::FocusChangeObserver* observer) { + focus_observers_.AddObserver(observer); +} + +void HeadlessFocusClient::RemoveObserver( + aura::client::FocusChangeObserver* observer) { + focus_observers_.RemoveObserver(observer); +} + +void HeadlessFocusClient::FocusWindow(aura::Window* window) { + if (window && !window->CanFocus()) + return; + + if (focused_window_) + observer_manager_.Remove(focused_window_); + aura::Window* old_focused_window = focused_window_; + focused_window_ = window; + if (focused_window_) + observer_manager_.Add(focused_window_); + + for (aura::client::FocusChangeObserver& observer : focus_observers_) + observer.OnWindowFocused(focused_window_, old_focused_window); + aura::client::FocusChangeObserver* observer = + aura::client::GetFocusChangeObserver(old_focused_window); + if (observer) + observer->OnWindowFocused(focused_window_, old_focused_window); + observer = aura::client::GetFocusChangeObserver(focused_window_); + if (observer) + observer->OnWindowFocused(focused_window_, old_focused_window); +} + +void HeadlessFocusClient::ResetFocusWithinActiveWindow(aura::Window* window) { + if (!window->Contains(focused_window_)) + FocusWindow(window); +} + +aura::Window* HeadlessFocusClient::GetFocusedWindow() { + return focused_window_; +} + +void HeadlessFocusClient::OnWindowDestroying(aura::Window* window) { + DCHECK_EQ(window, focused_window_); + FocusWindow(NULL); +} + +} // namespace headless diff --git a/chromium/headless/lib/browser/headless_focus_client.h b/chromium/headless/lib/browser/headless_focus_client.h new file mode 100644 index 00000000000..a218089ec7d --- /dev/null +++ b/chromium/headless/lib/browser/headless_focus_client.h @@ -0,0 +1,42 @@ +// Copyright 2017 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. + +#ifndef HEADLESS_LIB_BROWSER_HEADLESS_FOCUS_CLIENT_H_ +#define HEADLESS_LIB_BROWSER_HEADLESS_FOCUS_CLIENT_H_ + +#include "base/macros.h" +#include "base/observer_list.h" +#include "base/scoped_observer.h" +#include "ui/aura/client/focus_client.h" +#include "ui/aura/window_observer.h" + +namespace headless { + +class HeadlessFocusClient : public aura::client::FocusClient, + public aura::WindowObserver { + public: + HeadlessFocusClient(); + ~HeadlessFocusClient() override; + + private: + // Overridden from aura::client::FocusClient: + void AddObserver(aura::client::FocusChangeObserver* observer) override; + void RemoveObserver(aura::client::FocusChangeObserver* observer) override; + void FocusWindow(aura::Window* window) override; + void ResetFocusWithinActiveWindow(aura::Window* window) override; + aura::Window* GetFocusedWindow() override; + + // Overridden from aura::WindowObserver: + void OnWindowDestroying(aura::Window* window) override; + + aura::Window* focused_window_; + ScopedObserver<aura::Window, aura::WindowObserver> observer_manager_; + base::ObserverList<aura::client::FocusChangeObserver> focus_observers_; + + DISALLOW_COPY_AND_ASSIGN(HeadlessFocusClient); +}; + +} // namespace headless + +#endif // HEADLESS_LIB_BROWSER_HEADLESS_FOCUS_CLIENT_H_ diff --git a/chromium/headless/lib/browser/headless_screen.cc b/chromium/headless/lib/browser/headless_screen.cc index 9cc1e4745a5..1d329885800 100644 --- a/chromium/headless/lib/browser/headless_screen.cc +++ b/chromium/headless/lib/browser/headless_screen.cc @@ -9,8 +9,6 @@ #include "base/logging.h" #include "ui/aura/env.h" #include "ui/aura/window.h" -#include "ui/aura/window_event_dispatcher.h" -#include "ui/aura/window_tree_host.h" #include "ui/base/ime/input_method.h" #include "ui/gfx/geometry/rect_conversions.h" #include "ui/gfx/geometry/size_conversions.h" @@ -18,15 +16,6 @@ namespace headless { -namespace { - -bool IsRotationPortrait(display::Display::Rotation rotation) { - return rotation == display::Display::ROTATE_90 || - rotation == display::Display::ROTATE_270; -} - -} // namespace - // static HeadlessScreen* HeadlessScreen::Create(const gfx::Size& size) { return new HeadlessScreen(gfx::Rect(size)); @@ -34,101 +23,6 @@ HeadlessScreen* HeadlessScreen::Create(const gfx::Size& size) { HeadlessScreen::~HeadlessScreen() {} -aura::WindowTreeHost* HeadlessScreen::CreateHostForPrimaryDisplay() { - DCHECK(!host_); - host_ = aura::WindowTreeHost::Create( - gfx::Rect(GetPrimaryDisplay().GetSizeInPixel())); - // Some tests don't correctly manage window focus/activation states. - // Makes sure InputMethod is default focused so that IME basics can work. - host_->GetInputMethod()->OnFocus(); - host_->window()->AddObserver(this); - host_->InitHost(); - host_->window()->Show(); - return host_; -} - -void HeadlessScreen::SetDeviceScaleFactor(float device_scale_factor) { - display::Display display(GetPrimaryDisplay()); - gfx::Rect bounds_in_pixel(display.GetSizeInPixel()); - display.SetScaleAndBounds(device_scale_factor, bounds_in_pixel); - display_list().UpdateDisplay(display); -} - -void HeadlessScreen::SetDisplayRotation(display::Display::Rotation rotation) { - display::Display display(GetPrimaryDisplay()); - gfx::Rect bounds_in_pixel(display.GetSizeInPixel()); - gfx::Rect new_bounds(bounds_in_pixel); - if (IsRotationPortrait(rotation) != IsRotationPortrait(display.rotation())) { - new_bounds.set_width(bounds_in_pixel.height()); - new_bounds.set_height(bounds_in_pixel.width()); - } - display.set_rotation(rotation); - display.SetScaleAndBounds(display.device_scale_factor(), new_bounds); - display_list().UpdateDisplay(display); - host_->SetRootTransform(GetRotationTransform() * GetUIScaleTransform()); -} - -void HeadlessScreen::SetUIScale(float ui_scale) { - ui_scale_ = ui_scale; - display::Display display(GetPrimaryDisplay()); - gfx::Rect bounds_in_pixel(display.GetSizeInPixel()); - gfx::Rect new_bounds = gfx::ToNearestRect( - gfx::ScaleRect(gfx::RectF(bounds_in_pixel), 1.0f / ui_scale)); - display.SetScaleAndBounds(display.device_scale_factor(), new_bounds); - display_list().UpdateDisplay(display); - host_->SetRootTransform(GetRotationTransform() * GetUIScaleTransform()); -} - -void HeadlessScreen::SetWorkAreaInsets(const gfx::Insets& insets) { - display::Display display(GetPrimaryDisplay()); - display.UpdateWorkAreaFromInsets(insets); - display_list().UpdateDisplay(display); -} - -gfx::Transform HeadlessScreen::GetRotationTransform() const { - gfx::Transform rotate; - display::Display display(GetPrimaryDisplay()); - switch (display.rotation()) { - case display::Display::ROTATE_0: - break; - case display::Display::ROTATE_90: - rotate.Translate(display.bounds().height(), 0); - rotate.Rotate(90); - break; - case display::Display::ROTATE_270: - rotate.Translate(0, display.bounds().width()); - rotate.Rotate(270); - break; - case display::Display::ROTATE_180: - rotate.Translate(display.bounds().width(), display.bounds().height()); - rotate.Rotate(180); - break; - } - - return rotate; -} - -gfx::Transform HeadlessScreen::GetUIScaleTransform() const { - gfx::Transform ui_scale; - ui_scale.Scale(1.0f / ui_scale_, 1.0f / ui_scale_); - return ui_scale; -} - -void HeadlessScreen::OnWindowBoundsChanged(aura::Window* window, - const gfx::Rect& old_bounds, - const gfx::Rect& new_bounds) { - DCHECK_EQ(host_->window(), window); - display::Display display(GetPrimaryDisplay()); - display.SetSize(gfx::ScaleToFlooredSize(new_bounds.size(), - display.device_scale_factor())); - display_list().UpdateDisplay(display); -} - -void HeadlessScreen::OnWindowDestroying(aura::Window* window) { - if (host_->window() == window) - host_ = NULL; -} - gfx::Point HeadlessScreen::GetCursorScreenPoint() { return aura::Env::GetInstance()->last_mouse_location(); } @@ -139,18 +33,15 @@ bool HeadlessScreen::IsWindowUnderCursor(gfx::NativeWindow window) { gfx::NativeWindow HeadlessScreen::GetWindowAtScreenPoint( const gfx::Point& point) { - if (!host_ || !host_->window()) - return nullptr; - return host_->window()->GetTopWindowContainingPoint(point); + return nullptr; } display::Display HeadlessScreen::GetDisplayNearestWindow( - gfx::NativeWindow window) const { + gfx::NativeView window) const { return GetPrimaryDisplay(); } -HeadlessScreen::HeadlessScreen(const gfx::Rect& screen_bounds) - : host_(NULL), ui_scale_(1.0f) { +HeadlessScreen::HeadlessScreen(const gfx::Rect& screen_bounds) { static int64_t synthesized_display_id = 2000; display::Display display(synthesized_display_id++); display.SetScaleAndBounds(1.0f, screen_bounds); diff --git a/chromium/headless/lib/browser/headless_screen.h b/chromium/headless/lib/browser/headless_screen.h index 8c5cb98c083..b8331a67b0b 100644 --- a/chromium/headless/lib/browser/headless_screen.h +++ b/chromium/headless/lib/browser/headless_screen.h @@ -12,41 +12,18 @@ #include "ui/display/screen_base.h" namespace gfx { -class Insets; class Rect; -class Transform; -} - -namespace aura { -class Window; -class WindowTreeHost; } namespace headless { -class HeadlessScreen : public display::ScreenBase, public aura::WindowObserver { +class HeadlessScreen : public display::ScreenBase { public: // Creates a display::Screen of the specified size (physical pixels). static HeadlessScreen* Create(const gfx::Size& size); ~HeadlessScreen() override; - aura::WindowTreeHost* CreateHostForPrimaryDisplay(); - - void SetDeviceScaleFactor(float device_scale_fator); - void SetDisplayRotation(display::Display::Rotation rotation); - void SetUIScale(float ui_scale); - void SetWorkAreaInsets(const gfx::Insets& insets); - protected: - gfx::Transform GetRotationTransform() const; - gfx::Transform GetUIScaleTransform() const; - - // WindowObserver overrides: - void OnWindowBoundsChanged(aura::Window* window, - const gfx::Rect& old_bounds, - const gfx::Rect& new_bounds) override; - void OnWindowDestroying(aura::Window* window) override; - // display::Screen overrides: gfx::Point GetCursorScreenPoint() override; bool IsWindowUnderCursor(gfx::NativeWindow window) override; @@ -56,9 +33,6 @@ class HeadlessScreen : public display::ScreenBase, public aura::WindowObserver { private: explicit HeadlessScreen(const gfx::Rect& screen_bounds); - aura::WindowTreeHost* host_; - float ui_scale_; - DISALLOW_COPY_AND_ASSIGN(HeadlessScreen); }; diff --git a/chromium/headless/lib/browser/headless_web_contents_impl.cc b/chromium/headless/lib/browser/headless_web_contents_impl.cc index 1516d964ff3..710fc84c4c9 100644 --- a/chromium/headless/lib/browser/headless_web_contents_impl.cc +++ b/chromium/headless/lib/browser/headless_web_contents_impl.cc @@ -21,7 +21,7 @@ #include "content/public/browser/render_frame_host.h" #include "content/public/browser/render_process_host.h" #include "content/public/browser/render_view_host.h" -#include "content/public/browser/render_widget_host_view.h" +#include "content/public/browser/render_widget_host.h" #include "content/public/browser/web_contents.h" #include "content/public/browser/web_contents_delegate.h" #include "content/public/common/bindings_policy.h" @@ -31,7 +31,6 @@ #include "headless/lib/browser/headless_browser_main_parts.h" #include "headless/lib/browser/headless_devtools_client_impl.h" #include "services/service_manager/public/cpp/interface_registry.h" -#include "ui/aura/window.h" namespace headless { @@ -99,6 +98,10 @@ class HeadlessWebContentsImpl::Delegate : public content::WebContentsDelegate { security_style_explanations); } + void ActivateContents(content::WebContents* contents) override { + contents->GetRenderViewHost()->GetWidget()->Focus(); + } + private: HeadlessBrowserContextImpl* browser_context_; // Not owned. DISALLOW_COPY_AND_ASSIGN(Delegate); @@ -106,8 +109,7 @@ class HeadlessWebContentsImpl::Delegate : public content::WebContentsDelegate { // static std::unique_ptr<HeadlessWebContentsImpl> HeadlessWebContentsImpl::Create( - HeadlessWebContents::Builder* builder, - aura::Window* parent_window) { + HeadlessWebContents::Builder* builder) { content::WebContents::CreateParams create_params(builder->browser_context_, nullptr); create_params.initial_size = builder->window_size_; @@ -118,7 +120,7 @@ std::unique_ptr<HeadlessWebContentsImpl> HeadlessWebContentsImpl::Create( builder->browser_context_)); headless_web_contents->mojo_services_ = std::move(builder->mojo_services_); - headless_web_contents->InitializeScreen(parent_window, builder->window_size_); + headless_web_contents->InitializeScreen(builder->window_size_); if (!headless_web_contents->OpenURL(builder->initial_url_)) return nullptr; return headless_web_contents; @@ -136,18 +138,8 @@ HeadlessWebContentsImpl::CreateFromWebContents( return headless_web_contents; } -void HeadlessWebContentsImpl::InitializeScreen(aura::Window* parent_window, - const gfx::Size& initial_size) { - aura::Window* contents = web_contents_->GetNativeView(); - DCHECK(!parent_window->Contains(contents)); - parent_window->AddChild(contents); - contents->Show(); - - contents->SetBounds(gfx::Rect(initial_size)); - content::RenderWidgetHostView* host_view = - web_contents_->GetRenderWidgetHostView(); - if (host_view) - host_view->SetSize(initial_size); +void HeadlessWebContentsImpl::InitializeScreen(const gfx::Size& initial_size) { + browser()->PlatformInitializeWebContents(initial_size, web_contents_.get()); } HeadlessWebContentsImpl::HeadlessWebContentsImpl( @@ -173,8 +165,7 @@ HeadlessWebContentsImpl::~HeadlessWebContentsImpl() { void HeadlessWebContentsImpl::RenderFrameCreated( content::RenderFrameHost* render_frame_host) { if (!mojo_services_.empty()) { - render_frame_host->GetRenderViewHost()->AllowBindings( - content::BINDINGS_POLICY_HEADLESS); + render_frame_host->AllowBindings(content::BINDINGS_POLICY_HEADLESS); } service_manager::InterfaceRegistry* interface_registry = diff --git a/chromium/headless/lib/browser/headless_web_contents_impl.h b/chromium/headless/lib/browser/headless_web_contents_impl.h index a18a46aa49d..3346c64dca9 100644 --- a/chromium/headless/lib/browser/headless_web_contents_impl.h +++ b/chromium/headless/lib/browser/headless_web_contents_impl.h @@ -15,10 +15,6 @@ #include "headless/public/headless_devtools_target.h" #include "headless/public/headless_web_contents.h" -namespace aura { -class Window; -} - namespace content { class DevToolsAgentHost; class WebContents; @@ -42,8 +38,7 @@ class HeadlessWebContentsImpl : public HeadlessWebContents, static HeadlessWebContentsImpl* From(HeadlessWebContents* web_contents); static std::unique_ptr<HeadlessWebContentsImpl> Create( - HeadlessWebContents::Builder* builder, - aura::Window* parent_window); + HeadlessWebContents::Builder* builder); // Takes ownership of |web_contents|. static std::unique_ptr<HeadlessWebContentsImpl> CreateFromWebContents( @@ -85,8 +80,7 @@ class HeadlessWebContentsImpl : public HeadlessWebContents, HeadlessWebContentsImpl(content::WebContents* web_contents, HeadlessBrowserContextImpl* browser_context); - void InitializeScreen(aura::Window* parent_window, - const gfx::Size& initial_size); + void InitializeScreen(const gfx::Size& initial_size); using MojoService = HeadlessWebContents::Builder::MojoService; diff --git a/chromium/headless/lib/browser/headless_window_tree_host.cc b/chromium/headless/lib/browser/headless_window_tree_host.cc index 96cec1a96e6..5cae74e7d48 100644 --- a/chromium/headless/lib/browser/headless_window_tree_host.cc +++ b/chromium/headless/lib/browser/headless_window_tree_host.cc @@ -3,6 +3,7 @@ // found in the LICENSE file. #include "headless/lib/browser/headless_window_tree_host.h" +#include "ui/aura/window.h" #include "ui/gfx/icc_profile.h" @@ -15,10 +16,15 @@ HeadlessWindowTreeHost::HeadlessWindowTreeHost(const gfx::Rect& bounds) } HeadlessWindowTreeHost::~HeadlessWindowTreeHost() { + window_parenting_client_.reset(); DestroyCompositor(); DestroyDispatcher(); } +void HeadlessWindowTreeHost::SetParentWindow(gfx::NativeWindow window) { + window_parenting_client_.reset(new HeadlessWindowParentingClient(window)); +} + bool HeadlessWindowTreeHost::CanDispatchEvent(const ui::PlatformEvent& event) { return false; } diff --git a/chromium/headless/lib/browser/headless_window_tree_host.h b/chromium/headless/lib/browser/headless_window_tree_host.h index c01545c9c35..0aadfc87d18 100644 --- a/chromium/headless/lib/browser/headless_window_tree_host.h +++ b/chromium/headless/lib/browser/headless_window_tree_host.h @@ -5,7 +5,10 @@ #ifndef HEADLESS_LIB_BROWSER_HEADLESS_WINDOW_TREE_HOST_H_ #define HEADLESS_LIB_BROWSER_HEADLESS_WINDOW_TREE_HOST_H_ +#include <memory> + #include "base/macros.h" +#include "headless/lib/browser/headless_window_parenting_client.h" #include "ui/aura/window_tree_host.h" #include "ui/events/platform/platform_event_dispatcher.h" #include "ui/gfx/geometry/rect.h" @@ -18,6 +21,8 @@ class HeadlessWindowTreeHost : public aura::WindowTreeHost, explicit HeadlessWindowTreeHost(const gfx::Rect& bounds); ~HeadlessWindowTreeHost() override; + void SetParentWindow(gfx::NativeWindow window); + // ui::PlatformEventDispatcher: bool CanDispatchEvent(const ui::PlatformEvent& event) override; uint32_t DispatchEvent(const ui::PlatformEvent& event) override; @@ -39,6 +44,7 @@ class HeadlessWindowTreeHost : public aura::WindowTreeHost, private: gfx::Rect bounds_; + std::unique_ptr<aura::client::WindowParentingClient> window_parenting_client_; DISALLOW_COPY_AND_ASSIGN(HeadlessWindowTreeHost); }; diff --git a/chromium/headless/lib/embed_data.py b/chromium/headless/lib/embed_data.py new file mode 100644 index 00000000000..57ab26850ee --- /dev/null +++ b/chromium/headless/lib/embed_data.py @@ -0,0 +1,96 @@ +# Copyright 2017 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. + +import argparse +import sys +import os + +COPYRIGHT="""// Copyright 2017 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. +""" + +HEADER="""{copyright} + +#include "headless/lib/util/embedded_file.h" + +namespace {namespace} {{ + +extern const headless::util::EmbeddedFile {variable_name}; + +}} // namespace {namespace} +""" + +SOURCE="""{copyright} + +#include "{header_file}" + +namespace {{ + +const uint8_t contents[] = {contents}; + +}} // anonymous namespace + +namespace {namespace} {{ + +const headless::util::EmbeddedFile {variable_name} = {{ {length}, contents }}; + +}} // namespace {namespace} +""" + +def ParseArguments(args): + cmdline_parser = argparse.ArgumentParser() + cmdline_parser.add_argument('--data_file', required=True) + cmdline_parser.add_argument('--gendir', required=True) + cmdline_parser.add_argument('--header_file', required=True) + cmdline_parser.add_argument('--source_file', required=True) + cmdline_parser.add_argument('--namespace', required=True) + cmdline_parser.add_argument('--variable_name', required=True) + + return cmdline_parser.parse_args(args) + + +def GenerateArray(filepath): + with open(filepath, 'rb') as f: + contents = f.read() + + contents = [ str(ord(char)) for char in contents ] + + return len(contents), '{' + ','.join(contents) + '}' + + +def GenerateHeader(args): + return HEADER.format( + copyright=COPYRIGHT, + namespace=args.namespace, + variable_name=args.variable_name) + +def GenerateSource(args): + length, contents = GenerateArray(args.data_file) + + return SOURCE.format( + copyright=COPYRIGHT, + header_file=args.header_file, + namespace=args.namespace, + length=length, + contents=contents, + variable_name=args.variable_name) + + +def WriteHeader(args): + with open(os.path.join(args.gendir, args.header_file), 'w') as f: + f.write(GenerateHeader(args)) + + +def WriteSource(args): + with open(os.path.join(args.gendir, args.source_file), 'w') as f: + f.write(GenerateSource(args)) + + +if __name__ == '__main__': + args = ParseArguments(sys.argv[1:]) + + WriteHeader(args) + WriteSource(args) + diff --git a/chromium/headless/lib/embedder_mojo_browsertest.cc b/chromium/headless/lib/embedder_mojo_browsertest.cc index 92800f1acc3..17ce6ebac21 100644 --- a/chromium/headless/lib/embedder_mojo_browsertest.cc +++ b/chromium/headless/lib/embedder_mojo_browsertest.cc @@ -5,6 +5,7 @@ #include <memory> #include "base/optional.h" #include "base/path_service.h" +#include "base/run_loop.h" #include "base/strings/string_piece.h" #include "base/strings/stringprintf.h" #include "base/threading/thread_restrictions.h" @@ -66,7 +67,7 @@ class EmbedderMojoTest : public HeadlessBrowserTest, pak_path, ui::SCALE_FACTOR_NONE); } - // HeadlessWebContentsObserver implementation: + // HeadlessWebContents::Observer implementation: void DevToolsTargetReady() override { EXPECT_TRUE(web_contents_->GetDevToolsTarget()); web_contents_->GetDevToolsTarget()->AttachClient(devtools_client_.get()); @@ -225,12 +226,16 @@ class HttpDisabledByDefaultWhenMojoBindingsUsed : public EmbedderMojoTest, } void RunMojoTest() override { + base::RunLoop run_loop; devtools_client_->GetNetwork()->AddObserver(this); - devtools_client_->GetNetwork()->Enable(); - } - - GURL GetInitialUrl() const override { - return embedded_test_server()->GetURL("/page_one.html"); + devtools_client_->GetNetwork()->Enable(run_loop.QuitClosure()); + base::MessageLoop::ScopedNestableTaskAllower nest_loop( + base::MessageLoop::current()); + run_loop.Run(); + devtools_client_->GetPage()->AddObserver(this); + devtools_client_->GetPage()->Enable(); + devtools_client_->GetPage()->Navigate( + embedded_test_server()->GetURL("/page_one.html").spec()); } void ReturnTestResult(const std::string& result) override { diff --git a/chromium/headless/lib/headless_browser_browsertest.cc b/chromium/headless/lib/headless_browser_browsertest.cc index 0bbf976849c..3c67da19387 100644 --- a/chromium/headless/lib/headless_browser_browsertest.cc +++ b/chromium/headless/lib/headless_browser_browsertest.cc @@ -5,11 +5,15 @@ #include <memory> #include "base/command_line.h" +#include "base/files/file_enumerator.h" #include "base/files/file_util.h" #include "base/memory/ptr_util.h" #include "base/strings/stringprintf.h" #include "base/threading/thread_restrictions.h" +#include "content/public/common/url_constants.h" #include "content/public/test/browser_test.h" +#include "headless/lib/headless_macros.h" +#include "headless/public/devtools/domains/inspector.h" #include "headless/public/devtools/domains/network.h" #include "headless/public/devtools/domains/page.h" #include "headless/public/headless_browser.h" @@ -574,7 +578,7 @@ IN_PROC_BROWSER_TEST_F(HeadlessBrowserTest, SetCookiesWithDevTools) { .SetPath("") .SetSecure(true) .SetHttpOnly(true) - .SetSameSite(network::CookieSameSite::STRICT) + .SetSameSite(network::CookieSameSite::EXACT) .SetExpirationDate(0) .Build(); CookieSetter cookie_setter(this, web_contents, @@ -642,4 +646,88 @@ IN_PROC_BROWSER_TEST_F(HeadlessBrowserTest, MAYBE_RendererCommandPrefixTest) { base::DeleteFile(launcher_stamp, false); } +class CrashReporterTest : public HeadlessBrowserTest, + public HeadlessWebContents::Observer, + inspector::ExperimentalObserver { + public: + CrashReporterTest() : devtools_client_(HeadlessDevToolsClient::Create()) {} + ~CrashReporterTest() override {} + + void SetUp() override { + base::ThreadRestrictions::SetIOAllowed(true); + base::CreateNewTempDirectory("CrashReporterTest", &crash_dumps_dir_); + EXPECT_FALSE(options()->enable_crash_reporter); + options()->enable_crash_reporter = true; + options()->crash_dumps_dir = crash_dumps_dir_; + HeadlessBrowserTest::SetUp(); + } + + void TearDown() override { + base::ThreadRestrictions::SetIOAllowed(true); + base::DeleteFile(crash_dumps_dir_, /* recursive */ false); + } + + // HeadlessWebContents::Observer implementation: + void DevToolsTargetReady() override { + EXPECT_TRUE(web_contents_->GetDevToolsTarget()); + web_contents_->GetDevToolsTarget()->AttachClient(devtools_client_.get()); + devtools_client_->GetInspector()->GetExperimental()->AddObserver(this); + } + + // inspector::ExperimentalObserver implementation: + void OnTargetCrashed(const inspector::TargetCrashedParams&) override { + FinishAsynchronousTest(); + } + + protected: + HeadlessBrowserContext* browser_context_ = nullptr; + HeadlessWebContents* web_contents_ = nullptr; + std::unique_ptr<HeadlessDevToolsClient> devtools_client_; + base::FilePath crash_dumps_dir_; +}; + +// TODO(skyostil): Minidump generation currently is only supported on Linux. +#if defined(HEADLESS_USE_BREAKPAD) +#define MAYBE_GenerateMinidump GenerateMinidump +#else +#define MAYBE_GenerateMinidump DISABLED_GenerateMinidump +#endif // defined(HEADLESS_USE_BREAKPAD) +IN_PROC_BROWSER_TEST_F(CrashReporterTest, MAYBE_GenerateMinidump) { + // Navigates a tab to chrome://crash and checks that a minidump is generated. + // Note that we only test renderer crashes here -- browser crashes need to be + // tested with a separate harness. + // + // The case where crash reporting is disabled is covered by + // HeadlessCrashObserverTest. + browser_context_ = browser()->CreateBrowserContextBuilder().Build(); + + web_contents_ = browser_context_->CreateWebContentsBuilder() + .SetInitialURL(GURL(content::kChromeUICrashURL)) + .Build(); + + web_contents_->AddObserver(this); + RunAsynchronousTest(); + + // The target has crashed and should no longer be there. + EXPECT_FALSE(web_contents_->GetDevToolsTarget()); + + // Check that one minidump got created. + { + base::ThreadRestrictions::SetIOAllowed(true); + base::FileEnumerator it(crash_dumps_dir_, /* recursive */ false, + base::FileEnumerator::FILES); + base::FilePath minidump = it.Next(); + EXPECT_FALSE(minidump.empty()); + EXPECT_EQ(".dmp", minidump.Extension()); + EXPECT_TRUE(it.Next().empty()); + } + + web_contents_->RemoveObserver(this); + web_contents_->Close(); + web_contents_ = nullptr; + + browser_context_->Close(); + browser_context_ = nullptr; +} + } // namespace headless diff --git a/chromium/headless/lib/headless_browser_context_browsertest.cc b/chromium/headless/lib/headless_browser_context_browsertest.cc index 49ab195c094..b7bfa6fcd56 100644 --- a/chromium/headless/lib/headless_browser_context_browsertest.cc +++ b/chromium/headless/lib/headless_browser_context_browsertest.cc @@ -196,7 +196,7 @@ IN_PROC_BROWSER_TEST_F(HeadlessBrowserTest, ContextProtocolHandler) { another_browser_context->CreateWebContentsBuilder() .SetInitialURL(GURL("http://not-an-actual-domain.tld/hello.html")) .Build(); - EXPECT_TRUE(WaitForLoad(web_contents)); + EXPECT_FALSE(WaitForLoad(web_contents)); EXPECT_TRUE(EvaluateScript(web_contents, "document.body.innerHTML") ->GetResult() ->GetValue() diff --git a/chromium/headless/lib/headless_content_client.cc b/chromium/headless/lib/headless_content_client.cc index 4269da62f0d..333f43a6ac9 100644 --- a/chromium/headless/lib/headless_content_client.cc +++ b/chromium/headless/lib/headless_content_client.cc @@ -14,6 +14,10 @@ HeadlessContentClient::HeadlessContentClient(HeadlessBrowser::Options* options) HeadlessContentClient::~HeadlessContentClient() {} +std::string HeadlessContentClient::GetProduct() const { + return options_->product_name_and_version; +} + std::string HeadlessContentClient::GetUserAgent() const { return options_->user_agent; } diff --git a/chromium/headless/lib/headless_content_client.h b/chromium/headless/lib/headless_content_client.h index 56775bd0b4d..eb4d07d41b6 100644 --- a/chromium/headless/lib/headless_content_client.h +++ b/chromium/headless/lib/headless_content_client.h @@ -16,6 +16,7 @@ class HeadlessContentClient : public content::ContentClient { ~HeadlessContentClient() override; // content::ContentClient implementation: + std::string GetProduct() const override; std::string GetUserAgent() const override; base::string16 GetLocalizedString(int message_id) const override; base::StringPiece GetDataResource( diff --git a/chromium/headless/lib/headless_content_main_delegate.cc b/chromium/headless/lib/headless_content_main_delegate.cc index 4b7c1609035..72df5a29a65 100644 --- a/chromium/headless/lib/headless_content_main_delegate.cc +++ b/chromium/headless/lib/headless_content_main_delegate.cc @@ -4,21 +4,31 @@ #include "headless/lib/headless_content_main_delegate.h" +#include "base/base_switches.h" #include "base/command_line.h" #include "base/files/file_util.h" +#include "base/lazy_instance.h" #include "base/path_service.h" #include "base/run_loop.h" +#include "base/strings/string_number_conversions.h" #include "base/trace_event/trace_event.h" +#include "components/crash/content/app/breakpad_linux.h" #include "content/public/browser/browser_main_runner.h" #include "content/public/common/content_switches.h" #include "headless/lib/browser/headless_browser_impl.h" #include "headless/lib/browser/headless_content_browser_client.h" +#include "headless/lib/headless_crash_reporter_client.h" +#include "headless/lib/headless_macros.h" #include "ui/base/resource/resource_bundle.h" #include "ui/base/ui_base_switches.h" #include "ui/gfx/switches.h" #include "ui/gl/gl_switches.h" #include "ui/ozone/public/ozone_switches.h" +#ifdef HEADLESS_USE_EMBEDDED_RESOURCES +#include "headless/embedded_resource_pak.h" +#endif + namespace headless { namespace { // Keep in sync with content/common/content_constants_internal.h. @@ -26,6 +36,9 @@ namespace { const int kTraceEventBrowserProcessSortIndex = -6; HeadlessContentMainDelegate* g_current_headless_content_main_delegate = nullptr; + +base::LazyInstance<HeadlessCrashReporterClient>::Leaky g_headless_crash_client = + LAZY_INSTANCE_INITIALIZER; } // namespace HeadlessContentMainDelegate::HeadlessContentMainDelegate( @@ -70,7 +83,96 @@ bool HeadlessContentMainDelegate::BasicStartupComplete(int* exit_code) { return false; } +void HeadlessContentMainDelegate::InitLogging( + const base::CommandLine& command_line) { +#if !defined(OS_WIN) + if (!command_line.HasSwitch(switches::kEnableLogging)) + return; +#endif + + logging::LoggingDestination log_mode; + base::FilePath log_filename(FILE_PATH_LITERAL("chrome_debug.log")); + if (command_line.GetSwitchValueASCII(switches::kEnableLogging) == "stderr") { + log_mode = logging::LOG_TO_SYSTEM_DEBUG_LOG; + } else { + base::FilePath custom_filename( + command_line.GetSwitchValuePath(switches::kEnableLogging)); + if (custom_filename.empty()) { + log_mode = logging::LOG_TO_ALL; + } else { + log_mode = logging::LOG_TO_FILE; + log_filename = custom_filename; + } + } + + if (command_line.HasSwitch(switches::kLoggingLevel) && + logging::GetMinLogLevel() >= 0) { + std::string log_level = + command_line.GetSwitchValueASCII(switches::kLoggingLevel); + int level = 0; + if (base::StringToInt(log_level, &level) && level >= 0 && + level < logging::LOG_NUM_SEVERITIES) { + logging::SetMinLogLevel(level); + } else { + DLOG(WARNING) << "Bad log level: " << log_level; + } + } + + base::FilePath log_path; + logging::LoggingSettings settings; + + if (PathService::Get(base::DIR_MODULE, &log_path)) { + log_path = log_path.Append(log_filename); + } else { + log_path = log_filename; + } + + const std::string process_type = + command_line.GetSwitchValueASCII(switches::kProcessType); + + settings.logging_dest = log_mode; + settings.log_file = log_path.value().c_str(); + settings.lock_log = logging::DONT_LOCK_LOG_FILE; + settings.delete_old = process_type.empty() ? logging::DELETE_OLD_LOG_FILE + : logging::APPEND_TO_OLD_LOG_FILE; + bool success = logging::InitLogging(settings); + DCHECK(success); +} + +void HeadlessContentMainDelegate::InitCrashReporter( + const base::CommandLine& command_line) { + const std::string process_type = + command_line.GetSwitchValueASCII(switches::kProcessType); + crash_reporter::SetCrashReporterClient(g_headless_crash_client.Pointer()); + g_headless_crash_client.Pointer()->set_crash_dumps_dir( + browser_->options()->crash_dumps_dir); + +#if !defined(OS_MACOSX) + if (!browser_->options()->enable_crash_reporter) { + DCHECK(!breakpad::IsCrashReporterEnabled()); + return; + } +#if defined(HEADLESS_USE_BREAKPAD) + if (process_type != switches::kZygoteProcess) + breakpad::InitCrashReporter(process_type); +#endif // defined(HEADLESS_USE_BREAKPAD) +#endif // !defined(OS_MACOSX) +} + void HeadlessContentMainDelegate::PreSandboxStartup() { + const base::CommandLine& command_line( + *base::CommandLine::ForCurrentProcess()); +#if defined(OS_WIN) + // Windows always needs to initialize logging, otherwise you get a renderer + // crash. + InitLogging(command_line); +#else + if (command_line.HasSwitch(switches::kEnableLogging)) + InitLogging(command_line); +#endif +#if !defined(OS_MACOSX) + InitCrashReporter(command_line); +#endif InitializeResourceBundle(); } @@ -100,9 +202,18 @@ int HeadlessContentMainDelegate::RunProcess( return 0; } +#if !defined(OS_MACOSX) && defined(OS_POSIX) && !defined(OS_ANDROID) void HeadlessContentMainDelegate::ZygoteForked() { - // TODO(skyostil): Disable the zygote host. + const base::CommandLine& command_line( + *base::CommandLine::ForCurrentProcess()); + const std::string process_type = + command_line.GetSwitchValueASCII(switches::kProcessType); + // Unconditionally try to turn on crash reporting since we do not have access + // to the latest browser options at this point when testing. Breakpad will + // bail out gracefully if the browser process hasn't enabled crash reporting. + breakpad::InitCrashReporter(process_type); } +#endif // static HeadlessContentMainDelegate* HeadlessContentMainDelegate::GetInstance() { @@ -121,6 +232,13 @@ void HeadlessContentMainDelegate::InitializeResourceBundle() { ui::ResourceBundle::InitSharedInstanceWithLocale( locale, nullptr, ui::ResourceBundle::DO_NOT_LOAD_COMMON_RESOURCES); +#ifdef HEADLESS_USE_EMBEDDED_RESOURCES + ResourceBundle::GetSharedInstance().AddDataPackFromBuffer( + base::StringPiece( + reinterpret_cast<const char*>(kHeadlessResourcePak.contents), + kHeadlessResourcePak.length), + ui::SCALE_FACTOR_NONE); +#else // Try loading the headless library pak file first. If it doesn't exist (i.e., // when we're running with the --headless switch), fall back to the browser's // resource pak. @@ -129,6 +247,7 @@ void HeadlessContentMainDelegate::InitializeResourceBundle() { pak_file = dir_module.Append(FILE_PATH_LITERAL("resources.pak")); ResourceBundle::GetSharedInstance().AddDataPackFromPath( pak_file, ui::SCALE_FACTOR_NONE); +#endif } content::ContentBrowserClient* diff --git a/chromium/headless/lib/headless_content_main_delegate.h b/chromium/headless/lib/headless_content_main_delegate.h index 2e7aafe7973..cb33f226d17 100644 --- a/chromium/headless/lib/headless_content_main_delegate.h +++ b/chromium/headless/lib/headless_content_main_delegate.h @@ -6,6 +6,7 @@ #define HEADLESS_LIB_HEADLESS_CONTENT_MAIN_DELEGATE_H_ #include <memory> +#include <string> #include "base/compiler_specific.h" #include "base/macros.h" @@ -13,6 +14,10 @@ #include "headless/lib/browser/headless_platform_event_source.h" #include "headless/lib/headless_content_client.h" +namespace base { +class CommandLine; +} + namespace headless { class HeadlessBrowserImpl; @@ -30,14 +35,19 @@ class HeadlessContentMainDelegate : public content::ContentMainDelegate { int RunProcess( const std::string& process_type, const content::MainFunctionParams& main_function_params) override; - void ZygoteForked() override; content::ContentBrowserClient* CreateContentBrowserClient() override; HeadlessBrowserImpl* browser() const { return browser_.get(); } +#if !defined(OS_MACOSX) && defined(OS_POSIX) && !defined(OS_ANDROID) + void ZygoteForked() override; +#endif + private: friend class HeadlessBrowserTest; + void InitLogging(const base::CommandLine& command_line); + void InitCrashReporter(const base::CommandLine& command_line); static void InitializeResourceBundle(); static HeadlessContentMainDelegate* GetInstance(); diff --git a/chromium/headless/lib/headless_crash_reporter_client.cc b/chromium/headless/lib/headless_crash_reporter_client.cc new file mode 100644 index 00000000000..a319161dbd1 --- /dev/null +++ b/chromium/headless/lib/headless_crash_reporter_client.cc @@ -0,0 +1,63 @@ +// Copyright 2017 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 "headless/lib/headless_crash_reporter_client.h" + +#include <utility> + +#include "base/command_line.h" +#include "base/path_service.h" +#include "base/strings/string16.h" +#include "base/strings/utf_string_conversions.h" +#include "build/build_config.h" +#include "content/public/common/content_switches.h" +#include "headless/public/version.h" + +namespace headless { + +HeadlessCrashReporterClient::HeadlessCrashReporterClient() = default; +HeadlessCrashReporterClient::~HeadlessCrashReporterClient() = default; + +#if defined(OS_POSIX) && !defined(OS_MACOSX) +void HeadlessCrashReporterClient::GetProductNameAndVersion( + const char** product_name, + const char** version) { + *product_name = "HeadlessChrome"; + *version = PRODUCT_VERSION; +} + +base::FilePath HeadlessCrashReporterClient::GetReporterLogFilename() { + return base::FilePath(FILE_PATH_LITERAL("uploads.log")); +} +#endif // defined(OS_POSIX) && !defined(OS_MACOSX) + +bool HeadlessCrashReporterClient::GetCrashDumpLocation( +#if defined(OS_WIN) + base::string16* crash_dir +#else + base::FilePath* crash_dir +#endif + ) { + base::FilePath crash_directory = crash_dumps_dir_; + if (crash_directory.empty() && + !base::PathService::Get(base::DIR_MODULE, &crash_directory)) { + return false; + } +#if defined(OS_WIN) + *crash_dir = crash_directory.value(); +#else + *crash_dir = std::move(crash_directory); +#endif + return true; +} + +bool HeadlessCrashReporterClient::EnableBreakpadForProcess( + const std::string& process_type) { + return process_type == switches::kRendererProcess || + process_type == switches::kPpapiPluginProcess || + process_type == switches::kZygoteProcess || + process_type == switches::kGpuProcess; +} + +} // namespace content diff --git a/chromium/headless/lib/headless_crash_reporter_client.h b/chromium/headless/lib/headless_crash_reporter_client.h new file mode 100644 index 00000000000..42dc35bb232 --- /dev/null +++ b/chromium/headless/lib/headless_crash_reporter_client.h @@ -0,0 +1,51 @@ +// Copyright 2017 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. + +#ifndef HEADLESS_LIB_HEADLESS_CRASH_REPORTER_CLIENT_H_ +#define HEADLESS_LIB_HEADLESS_CRASH_REPORTER_CLIENT_H_ + +#include "base/files/file_path.h" +#include "base/macros.h" +#include "base/strings/string16.h" +#include "build/build_config.h" +#include "components/crash/content/app/crash_reporter_client.h" + +namespace headless { + +class HeadlessCrashReporterClient : public crash_reporter::CrashReporterClient { + public: + HeadlessCrashReporterClient(); + ~HeadlessCrashReporterClient() override; + + void set_crash_dumps_dir(const base::FilePath& dir) { + crash_dumps_dir_ = dir; + } + const base::FilePath& crash_dumps_dir() const { return crash_dumps_dir_; } + +#if defined(OS_POSIX) && !defined(OS_MACOSX) + // Returns a textual description of the product type and version to include + // in the crash report. + void GetProductNameAndVersion(const char** product_name, + const char** version) override; + + base::FilePath GetReporterLogFilename() override; +#endif // defined(OS_POSIX) && !defined(OS_MACOSX) + +#if defined(OS_WIN) + bool GetCrashDumpLocation(base::string16* crash_dir) override; +#else + bool GetCrashDumpLocation(base::FilePath* crash_dir) override; +#endif + + bool EnableBreakpadForProcess(const std::string& process_type) override; + + private: + base::FilePath crash_dumps_dir_; + + DISALLOW_COPY_AND_ASSIGN(HeadlessCrashReporterClient); +}; + +} // namespace headless + +#endif // HEADLESS_LIB_HEADLESS_CRASH_REPORTER_CLIENT_H_ diff --git a/chromium/headless/lib/headless_devtools_client_browsertest.cc b/chromium/headless/lib/headless_devtools_client_browsertest.cc index 6dbd79fad28..0bb4bf1be6a 100644 --- a/chromium/headless/lib/headless_devtools_client_browsertest.cc +++ b/chromium/headless/lib/headless_devtools_client_browsertest.cc @@ -5,6 +5,7 @@ #include <memory> #include "base/json/json_reader.h" +#include "base/run_loop.h" #include "content/public/browser/render_widget_host_view.h" #include "content/public/browser/web_contents.h" #include "content/public/common/url_constants.h" @@ -21,6 +22,7 @@ #include "headless/public/headless_devtools_client.h" #include "headless/public/headless_devtools_target.h" #include "headless/test/headless_browser_test.h" +#include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" #include "url/gurl.h" @@ -30,6 +32,8 @@ EXPECT_EQ((expected).height(), (actual).height()); \ } while (false) +using testing::ElementsAre; + namespace headless { namespace { @@ -60,7 +64,11 @@ class HeadlessDevToolsClientNavigationTest .SetUrl(embedded_test_server()->GetURL("/hello.html").spec()) .Build(); devtools_client_->GetPage()->GetExperimental()->AddObserver(this); - devtools_client_->GetPage()->Enable(); + base::RunLoop run_loop; + base::MessageLoop::ScopedNestableTaskAllower nest_loop( + base::MessageLoop::current()); + devtools_client_->GetPage()->Enable(run_loop.QuitClosure()); + run_loop.Run(); devtools_client_->GetPage()->Navigate(std::move(params)); } @@ -153,8 +161,13 @@ class HeadlessDevToolsClientObserverTest public: void RunDevTooledTest() override { EXPECT_TRUE(embedded_test_server()->Start()); + base::RunLoop run_loop; devtools_client_->GetNetwork()->AddObserver(this); - devtools_client_->GetNetwork()->Enable(); + devtools_client_->GetNetwork()->Enable(run_loop.QuitClosure()); + base::MessageLoop::ScopedNestableTaskAllower nest_loop( + base::MessageLoop::current()); + run_loop.Run(); + devtools_client_->GetPage()->Navigate( embedded_test_server()->GetURL("/hello.html").spec()); } @@ -189,6 +202,12 @@ class HeadlessDevToolsClientExperimentalTest public: void RunDevTooledTest() override { EXPECT_TRUE(embedded_test_server()->Start()); + base::RunLoop run_loop; + devtools_client_->GetPage()->GetExperimental()->AddObserver(this); + devtools_client_->GetPage()->Enable(run_loop.QuitClosure()); + base::MessageLoop::ScopedNestableTaskAllower nest_loop( + base::MessageLoop::current()); + run_loop.Run(); // Check that experimental commands require parameter objects. devtools_client_->GetRuntime() ->GetExperimental() @@ -202,8 +221,6 @@ class HeadlessDevToolsClientExperimentalTest devtools_client_->GetRuntime()->GetExperimental()->RunIfWaitingForDebugger( runtime::RunIfWaitingForDebuggerParams::Builder().Build()); - devtools_client_->GetPage()->GetExperimental()->AddObserver(this); - devtools_client_->GetPage()->Enable(); devtools_client_->GetPage()->Navigate( embedded_test_server()->GetURL("/hello.html").spec()); } @@ -635,8 +652,12 @@ class HeadlessDevToolsNavigationControlTest public: void RunDevTooledTest() override { EXPECT_TRUE(embedded_test_server()->Start()); + base::RunLoop run_loop; devtools_client_->GetPage()->GetExperimental()->AddObserver(this); - devtools_client_->GetPage()->Enable(); + devtools_client_->GetPage()->Enable(run_loop.QuitClosure()); + base::MessageLoop::ScopedNestableTaskAllower nest_loop( + base::MessageLoop::current()); + run_loop.Run(); devtools_client_->GetPage()->GetExperimental()->SetControlNavigations( headless::page::SetControlNavigationsParams::Builder() .SetEnabled(true) @@ -758,8 +779,12 @@ class HeadlessDevToolsMethodCallErrorTest public: void RunDevTooledTest() override { EXPECT_TRUE(embedded_test_server()->Start()); + base::RunLoop run_loop; devtools_client_->GetPage()->AddObserver(this); - devtools_client_->GetPage()->Enable(); + devtools_client_->GetPage()->Enable(run_loop.QuitClosure()); + base::MessageLoop::ScopedNestableTaskAllower nest_loop( + base::MessageLoop::current()); + run_loop.Run(); devtools_client_->GetPage()->Navigate( embedded_test_server()->GetURL("/hello.html").spec()); } @@ -789,4 +814,67 @@ class HeadlessDevToolsMethodCallErrorTest HEADLESS_ASYNC_DEVTOOLED_TEST_F(HeadlessDevToolsMethodCallErrorTest); +class HeadlessDevToolsNetworkBlockedUrlTest + : public HeadlessAsyncDevTooledBrowserTest, + public page::Observer, + public network::Observer { + public: + void RunDevTooledTest() override { + EXPECT_TRUE(embedded_test_server()->Start()); + base::RunLoop run_loop; + devtools_client_->GetPage()->AddObserver(this); + devtools_client_->GetPage()->Enable(); + devtools_client_->GetNetwork()->AddObserver(this); + devtools_client_->GetNetwork()->Enable(run_loop.QuitClosure()); + base::MessageLoop::ScopedNestableTaskAllower nest_loop( + base::MessageLoop::current()); + run_loop.Run(); + devtools_client_->GetNetwork()->GetExperimental()->AddBlockedURL( + network::AddBlockedURLParams::Builder() + .SetUrl("dom_tree_test.css") + .Build()); + devtools_client_->GetPage()->Navigate( + embedded_test_server()->GetURL("/dom_tree_test.html").spec()); + } + + std::string GetUrlPath(const std::string& url) const { + GURL gurl(url); + return gurl.path(); + } + + void OnRequestWillBeSent( + const network::RequestWillBeSentParams& params) override { + std::string path = GetUrlPath(params.GetRequest()->GetUrl()); + requests_to_be_sent_.push_back(path); + request_id_to_path_[params.GetRequestId()] = path; + } + + void OnResponseReceived( + const network::ResponseReceivedParams& params) override { + responses_received_.push_back(GetUrlPath(params.GetResponse()->GetUrl())); + } + + void OnLoadingFailed(const network::LoadingFailedParams& failed) override { + failures_.push_back(request_id_to_path_[failed.GetRequestId()]); + EXPECT_EQ(network::BlockedReason::INSPECTOR, failed.GetBlockedReason()); + } + + void OnLoadEventFired(const page::LoadEventFiredParams&) override { + EXPECT_THAT(requests_to_be_sent_, + ElementsAre("/dom_tree_test.html", "/dom_tree_test.css", + "/iframe.html")); + EXPECT_THAT(responses_received_, + ElementsAre("/dom_tree_test.html", "/iframe.html")); + EXPECT_THAT(failures_, ElementsAre("/dom_tree_test.css")); + FinishAsynchronousTest(); + } + + std::map<std::string, std::string> request_id_to_path_; + std::vector<std::string> requests_to_be_sent_; + std::vector<std::string> responses_received_; + std::vector<std::string> failures_; +}; + +HEADLESS_ASYNC_DEVTOOLED_TEST_F(HeadlessDevToolsNetworkBlockedUrlTest); + } // namespace headless diff --git a/chromium/headless/lib/headless_macros.h b/chromium/headless/lib/headless_macros.h new file mode 100644 index 00000000000..4b7bedb7a9c --- /dev/null +++ b/chromium/headless/lib/headless_macros.h @@ -0,0 +1,12 @@ +// Copyright 2017 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. + +#ifndef HEADLESS_LIB_HEADLESS_MACROS_H_ +#define HEADLESS_LIB_HEADLESS_MACROS_H_ + +#if defined(OS_POSIX) && !defined(OS_MACOSX) +#define HEADLESS_USE_BREAKPAD +#endif // defined(OS_POSIX) && !defined(OS_MACOSX) + +#endif // HEADLESS_LIB_HEADLESS_MACROS_H_ diff --git a/chromium/headless/lib/headless_web_contents_browsertest.cc b/chromium/headless/lib/headless_web_contents_browsertest.cc index 693e2dceeaa..3bb62652ee9 100644 --- a/chromium/headless/lib/headless_web_contents_browsertest.cc +++ b/chromium/headless/lib/headless_web_contents_browsertest.cc @@ -61,6 +61,45 @@ IN_PROC_BROWSER_TEST_F(HeadlessWebContentsTest, WindowOpen) { browser_context->GetAllWebContents().size()); } +IN_PROC_BROWSER_TEST_F(HeadlessWebContentsTest, Focus) { + EXPECT_TRUE(embedded_test_server()->Start()); + + HeadlessBrowserContext* browser_context = + browser()->CreateBrowserContextBuilder().Build(); + + HeadlessWebContents* web_contents = + browser_context->CreateWebContentsBuilder() + .SetInitialURL(embedded_test_server()->GetURL("/hello.html")) + .Build(); + EXPECT_TRUE(WaitForLoad(web_contents)); + + bool result; + EXPECT_TRUE(EvaluateScript(web_contents, "document.hasFocus()") + ->GetResult() + ->GetValue() + ->GetAsBoolean(&result)); + EXPECT_TRUE(result); + + HeadlessWebContents* web_contents2 = + browser_context->CreateWebContentsBuilder() + .SetInitialURL(embedded_test_server()->GetURL("/hello.html")) + .Build(); + EXPECT_TRUE(WaitForLoad(web_contents2)); + + // TODO(irisu): Focus of two web contents should be independent of the other. + // Both web_contents and web_contents2 should be focused at this point. + EXPECT_TRUE(EvaluateScript(web_contents, "document.hasFocus()") + ->GetResult() + ->GetValue() + ->GetAsBoolean(&result)); + EXPECT_FALSE(result); + EXPECT_TRUE(EvaluateScript(web_contents2, "document.hasFocus()") + ->GetResult() + ->GetValue() + ->GetAsBoolean(&result)); + EXPECT_TRUE(result); +} + namespace { bool DecodePNG(std::string base64_data, SkBitmap* bitmap) { std::string png_data; diff --git a/chromium/headless/lib/util/embedded_file.h b/chromium/headless/lib/util/embedded_file.h new file mode 100644 index 00000000000..4647aefec0a --- /dev/null +++ b/chromium/headless/lib/util/embedded_file.h @@ -0,0 +1,22 @@ +// Copyright 2017 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. + +#ifndef HEADLESS_LIB_UTIL_EMBEDDED_FILE_H_ +#define HEADLESS_LIB_UTIL_EMBEDDED_FILE_H_ + +#include <cstdint> +#include <cstdlib> + +namespace headless { +namespace util { + +struct EmbeddedFile { + size_t length; + const uint8_t* contents; +}; + +} // namespace util +} // namespace headless + +#endif // HEADLESS_LIB_UTIL_EMBEDDED_FILE_H_ diff --git a/chromium/headless/public/domains/types_unittest.cc b/chromium/headless/public/domains/types_unittest.cc index db07df1a58d..7d33c2b5db7 100644 --- a/chromium/headless/public/domains/types_unittest.cc +++ b/chromium/headless/public/domains/types_unittest.cc @@ -197,7 +197,7 @@ TEST(TypesTest, ObjectPropertyParseError) { } TEST(TypesTest, AnyProperty) { - std::unique_ptr<base::Value> value(new base::FundamentalValue(123)); + std::unique_ptr<base::Value> value(new base::Value(123)); std::unique_ptr<accessibility::AXValue> object( accessibility::AXValue::Builder() .SetType(accessibility::AXValueType::INTEGER) diff --git a/chromium/headless/public/headless_browser.cc b/chromium/headless/public/headless_browser.cc index 2b4270bd591..caa24e794d9 100644 --- a/chromium/headless/public/headless_browser.cc +++ b/chromium/headless/public/headless_browser.cc @@ -7,17 +7,22 @@ #include <utility> #include "content/public/common/user_agent.h" +#include "headless/public/version.h" using Options = headless::HeadlessBrowser::Options; using Builder = headless::HeadlessBrowser::Options::Builder; namespace headless { -// Product name for building the default user agent string. namespace { +// Product name for building the default user agent string. const char kProductName[] = "HeadlessChrome"; constexpr gfx::Size kDefaultWindowSize(800, 600); + +std::string GetProductNameAndVersion() { + return std::string(kProductName) + "/" + PRODUCT_VERSION; } +} // namespace Options::Options(int argc, const char** argv) : argc(argc), @@ -26,9 +31,11 @@ Options::Options(int argc, const char** argv) single_process_mode(false), disable_sandbox(false), gl_implementation("osmesa"), - user_agent(content::BuildUserAgentFromProduct(kProductName)), + product_name_and_version(GetProductNameAndVersion()), + user_agent(content::BuildUserAgentFromProduct(product_name_and_version)), window_size(kDefaultWindowSize), - incognito_mode(true) {} + incognito_mode(true), + enable_crash_reporter(false) {} Options::Options(Options&& options) = default; @@ -42,6 +49,12 @@ Builder::Builder() : options_(0, nullptr) {} Builder::~Builder() {} +Builder& Builder::SetProductNameAndVersion( + const std::string& product_name_and_version) { + options_.product_name_and_version = product_name_and_version; + return *this; +} + Builder& Builder::SetUserAgent(const std::string& user_agent) { options_.user_agent = user_agent; return *this; @@ -102,6 +115,22 @@ Builder& Builder::SetIncognitoMode(bool incognito_mode) { return *this; } +Builder& Builder::SetOverrideWebPreferencesCallback( + base::Callback<void(WebPreferences*)> callback) { + options_.override_web_preferences_callback = callback; + return *this; +} + +Builder& Builder::SetCrashReporterEnabled(bool enabled) { + options_.enable_crash_reporter = enabled; + return *this; +} + +Builder& Builder::SetCrashDumpsDir(const base::FilePath& dir) { + options_.crash_dumps_dir = dir; + return *this; +} + Options Builder::Build() { return std::move(options_); } diff --git a/chromium/headless/public/headless_browser.h b/chromium/headless/public/headless_browser.h index c3043f006ad..4b18f565b9e 100644 --- a/chromium/headless/public/headless_browser.h +++ b/chromium/headless/public/headless_browser.h @@ -128,6 +128,7 @@ struct HeadlessBrowser::Options { // Default per-context options, can be specialized on per-context basis. + std::string product_name_and_version; std::string user_agent; // Address of the HTTP/HTTPS proxy server to use. The system proxy settings @@ -149,6 +150,20 @@ struct HeadlessBrowser::Options { // Run a browser context in an incognito mode. Enabled by default. bool incognito_mode; + // Set a callback that is invoked to override WebPreferences for RenderViews + // created within the HeadlessBrowser. Called whenever the WebPreferences of a + // RenderView change. Executed on the browser main thread. + // + // WARNING: We cannot provide any guarantees about the stability of the + // exposed WebPreferences API, so use with care. + base::Callback<void(WebPreferences*)> override_web_preferences_callback; + + // Minidump crash reporter settings. Crash reporting is disabled by default. + // By default crash dumps are written to the directory containing the + // executable. + bool enable_crash_reporter; + base::FilePath crash_dumps_dir; + // Reminder: when adding a new field here, do not forget to add it to // HeadlessBrowserContextOptions (where appropriate). private: @@ -174,12 +189,18 @@ class HeadlessBrowser::Options::Builder { // Per-context settings. + Builder& SetProductNameAndVersion( + const std::string& product_name_and_version); Builder& SetUserAgent(const std::string& user_agent); Builder& SetProxyServer(const net::HostPortPair& proxy_server); Builder& SetHostResolverRules(const std::string& host_resolver_rules); Builder& SetWindowSize(const gfx::Size& window_size); Builder& SetUserDataDir(const base::FilePath& user_data_dir); Builder& SetIncognitoMode(bool incognito_mode); + Builder& SetOverrideWebPreferencesCallback( + base::Callback<void(WebPreferences*)> callback); + Builder& SetCrashReporterEnabled(bool enabled); + Builder& SetCrashDumpsDir(const base::FilePath& dir); Options Build(); diff --git a/chromium/headless/public/headless_browser_context.h b/chromium/headless/public/headless_browser_context.h index b779f6a38da..4bdd7a51d7e 100644 --- a/chromium/headless/public/headless_browser_context.h +++ b/chromium/headless/public/headless_browser_context.h @@ -103,24 +103,19 @@ class HEADLESS_EXPORT HeadlessBrowserContext::Builder { Builder& EnableUnsafeNetworkAccessWithMojoBindings( bool enable_http_and_https_if_mojo_used); - // Set a callback that is invoked to override WebPreferences for RenderViews - // created within this HeadlessBrowserContext. Called whenever the - // WebPreferences of a RenderView change. Executed on the browser main thread. - // - // WARNING: We cannot provide any guarantees about the stability of the - // exposed WebPreferences API, so use with care. - Builder& SetOverrideWebPreferencesCallback( - base::Callback<void(WebPreferences*)> callback); - // By default |HeadlessBrowserContext| inherits the following options from // the browser instance. The methods below can be used to override these // settings. See HeadlessBrowser::Options for their meaning. + Builder& SetProductNameAndVersion( + const std::string& product_name_and_version); Builder& SetUserAgent(const std::string& user_agent); Builder& SetProxyServer(const net::HostPortPair& proxy_server); Builder& SetHostResolverRules(const std::string& host_resolver_rules); Builder& SetWindowSize(const gfx::Size& window_size); Builder& SetUserDataDir(const base::FilePath& user_data_dir); Builder& SetIncognitoMode(bool incognito_mode); + Builder& SetOverrideWebPreferencesCallback( + base::Callback<void(WebPreferences*)> callback); HeadlessBrowserContext* Build(); diff --git a/chromium/headless/public/headless_shell.h b/chromium/headless/public/headless_shell.h index f083d2030c1..0fd9066a80d 100644 --- a/chromium/headless/public/headless_shell.h +++ b/chromium/headless/public/headless_shell.h @@ -11,8 +11,7 @@ namespace headless { // Start the Headless Shell application. Intended to be called early in main(). // Returns the exit code for the process. -int HEADLESS_EXPORT HeadlessShellMain(int argc, const char** argv); - +HEADLESS_EXPORT int HeadlessShellMain(int argc, const char** argv); } // namespace headless #endif // HEADLESS_PUBLIC_HEADLESS_SHELL_H_ diff --git a/chromium/headless/public/internal/value_conversions.h b/chromium/headless/public/internal/value_conversions.h index 78ac41fc4cb..948c94fee4d 100644 --- a/chromium/headless/public/internal/value_conversions.h +++ b/chromium/headless/public/internal/value_conversions.h @@ -32,17 +32,17 @@ struct FromValue { // partially specialize vector types. template <typename T> std::unique_ptr<base::Value> ToValueImpl(int value, T*) { - return base::MakeUnique<base::FundamentalValue>(value); + return base::MakeUnique<base::Value>(value); } template <typename T> std::unique_ptr<base::Value> ToValueImpl(double value, T*) { - return base::MakeUnique<base::FundamentalValue>(value); + return base::MakeUnique<base::Value>(value); } template <typename T> std::unique_ptr<base::Value> ToValueImpl(bool value, T*) { - return base::MakeUnique<base::FundamentalValue>(value); + return base::MakeUnique<base::Value>(value); } template <typename T> diff --git a/chromium/headless/public/util/deterministic_dispatcher.cc b/chromium/headless/public/util/deterministic_dispatcher.cc index 20982db898c..626279dd8b3 100644 --- a/chromium/headless/public/util/deterministic_dispatcher.cc +++ b/chromium/headless/public/util/deterministic_dispatcher.cc @@ -9,6 +9,7 @@ #include "base/bind.h" #include "base/logging.h" #include "headless/public/util/managed_dispatch_url_request_job.h" +#include "headless/public/util/navigation_request.h" namespace headless { @@ -16,20 +17,21 @@ DeterministicDispatcher::DeterministicDispatcher( scoped_refptr<base::SingleThreadTaskRunner> io_thread_task_runner) : io_thread_task_runner_(std::move(io_thread_task_runner)), dispatch_pending_(false), + navigation_in_progress_(false), weak_ptr_factory_(this) {} DeterministicDispatcher::~DeterministicDispatcher() {} void DeterministicDispatcher::JobCreated(ManagedDispatchURLRequestJob* job) { base::AutoLock lock(lock_); - pending_requests_.push_back(job); + pending_requests_.emplace_back(job); } void DeterministicDispatcher::JobKilled(ManagedDispatchURLRequestJob* job) { base::AutoLock lock(lock_); for (auto it = pending_requests_.begin(); it != pending_requests_.end(); it++) { - if (*it == job) { + if (it->url_request == job) { pending_requests_.erase(it); break; } @@ -56,10 +58,28 @@ void DeterministicDispatcher::JobDeleted(ManagedDispatchURLRequestJob* job) { MaybeDispatchJobLocked(); } +void DeterministicDispatcher::NavigationRequested( + std::unique_ptr<NavigationRequest> navigation) { + base::AutoLock lock(lock_); + pending_requests_.emplace_back(std::move(navigation)); + + MaybeDispatchJobLocked(); +} + void DeterministicDispatcher::MaybeDispatchJobLocked() { - if (dispatch_pending_ || ready_status_map_.empty()) + if (dispatch_pending_ || navigation_in_progress_) return; + if (ready_status_map_.empty()) { + if (pending_requests_.empty()) + return; // Nothing to do. + + // Don't post a task if the first job is a url request (which isn't ready + // yet). + if (pending_requests_.front().url_request) + return; + } + dispatch_pending_ = true; io_thread_task_runner_->PostTask( FROM_HERE, @@ -68,8 +88,8 @@ void DeterministicDispatcher::MaybeDispatchJobLocked() { } void DeterministicDispatcher::MaybeDispatchJobOnIOThreadTask() { - ManagedDispatchURLRequestJob* job; - net::Error job_status; + Request request; + net::Error job_status = net::ERR_FAILED; { base::AutoLock lock(lock_); @@ -77,22 +97,67 @@ void DeterministicDispatcher::MaybeDispatchJobOnIOThreadTask() { // If the job got deleted, |pending_requests_| may be empty. if (pending_requests_.empty()) return; - job = pending_requests_.front(); - StatusMap::const_iterator it = ready_status_map_.find(job); - // Bail out if the oldest job is not be ready for dispatch yet. - if (it == ready_status_map_.end()) + + // Bail out if we're waiting for a navigation to complete. + if (navigation_in_progress_) return; - job_status = it->second; - ready_status_map_.erase(it); + request = std::move(pending_requests_.front()); + if (request.url_request) { + StatusMap::const_iterator it = + ready_status_map_.find(request.url_request); + // Bail out if the oldest job is not be ready for dispatch yet. + if (it == ready_status_map_.end()) + return; + + job_status = it->second; + ready_status_map_.erase(it); + } else { + DCHECK(!navigation_in_progress_); + navigation_in_progress_ = true; + } pending_requests_.pop_front(); } - if (job_status == net::OK) { - job->OnHeadersComplete(); + if (request.url_request) { + if (job_status == net::OK) { + request.url_request->OnHeadersComplete(); + } else { + request.url_request->OnStartError(job_status); + } } else { - job->OnStartError(job_status); + request.navigation_request->StartProcessing( + base::Bind(&DeterministicDispatcher::NavigationDoneTask, + weak_ptr_factory_.GetWeakPtr())); } } +void DeterministicDispatcher::NavigationDoneTask() { + { + base::AutoLock lock(lock_); + DCHECK(navigation_in_progress_); + navigation_in_progress_ = false; + } + + MaybeDispatchJobLocked(); +} + +DeterministicDispatcher::Request::Request() : url_request(nullptr) {} +DeterministicDispatcher::Request::~Request() {} + +DeterministicDispatcher::Request::Request( + ManagedDispatchURLRequestJob* url_request) + : url_request(url_request) {} + +DeterministicDispatcher::Request::Request( + std::unique_ptr<NavigationRequest> navigation_request) + : url_request(nullptr), navigation_request(std::move(navigation_request)) {} + +DeterministicDispatcher::Request& DeterministicDispatcher::Request::operator=( + DeterministicDispatcher::Request&& other) { + url_request = other.url_request; + navigation_request = std::move(other.navigation_request); + return *this; +} + } // namespace headless diff --git a/chromium/headless/public/util/deterministic_dispatcher.h b/chromium/headless/public/util/deterministic_dispatcher.h index 0fea383b9cc..56c7c7e6390 100644 --- a/chromium/headless/public/util/deterministic_dispatcher.h +++ b/chromium/headless/public/util/deterministic_dispatcher.h @@ -20,8 +20,8 @@ namespace headless { class ManagedDispatchURLRequestJob; -// The purpose of this class is to queue up calls to OnHeadersComplete and -// OnStartError and dispatch them in order of URLRequestJob creation. This +// The purpose of this class is to queue up navigations and calls to +// OnHeadersComplete / OnStartError and dispatch them in order of creation. This // helps make renders deterministic at the cost of slower page loads. class DeterministicDispatcher : public URLRequestDispatcher { public: @@ -36,17 +36,34 @@ class DeterministicDispatcher : public URLRequestDispatcher { void JobFailed(ManagedDispatchURLRequestJob* job, net::Error error) override; void DataReady(ManagedDispatchURLRequestJob* job) override; void JobDeleted(ManagedDispatchURLRequestJob* job) override; + void NavigationRequested( + std::unique_ptr<NavigationRequest> navigation_request) override; private: + void MaybeDispatchNavigationJobLocked(); void MaybeDispatchJobLocked(); void MaybeDispatchJobOnIOThreadTask(); + void NavigationDoneTask(); scoped_refptr<base::SingleThreadTaskRunner> io_thread_task_runner_; // Protects all members below. base::Lock lock_; - std::deque<ManagedDispatchURLRequestJob*> pending_requests_; + // TODO(alexclarke): Use std::variant when c++17 is allowed in chromium. + struct Request { + Request(); + explicit Request(ManagedDispatchURLRequestJob* url_request); + explicit Request(std::unique_ptr<NavigationRequest> navigation_request); + ~Request(); + + Request& operator=(Request&& other); + + ManagedDispatchURLRequestJob* url_request; // NOT OWNED + std::unique_ptr<NavigationRequest> navigation_request; + }; + + std::deque<Request> pending_requests_; using StatusMap = std::map<ManagedDispatchURLRequestJob*, net::Error>; StatusMap ready_status_map_; @@ -54,6 +71,7 @@ class DeterministicDispatcher : public URLRequestDispatcher { // Whether or not a MaybeDispatchJobOnIoThreadTask has been posted on the // |io_thread_task_runner_| bool dispatch_pending_; + bool navigation_in_progress_; base::WeakPtrFactory<DeterministicDispatcher> weak_ptr_factory_; diff --git a/chromium/headless/public/util/deterministic_dispatcher_test.cc b/chromium/headless/public/util/deterministic_dispatcher_test.cc index 24cede8b2e9..7c2c60494bd 100644 --- a/chromium/headless/public/util/deterministic_dispatcher_test.cc +++ b/chromium/headless/public/util/deterministic_dispatcher_test.cc @@ -8,8 +8,10 @@ #include <string> #include <vector> +#include "base/memory/ptr_util.h" #include "base/run_loop.h" #include "base/single_thread_task_runner.h" +#include "headless/public/util/navigation_request.h" #include "headless/public/util/testing/fake_managed_dispatch_url_request_job.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" @@ -126,4 +128,70 @@ TEST_F(DeterministicDispatcherTest, JobKilled) { EXPECT_TRUE(notifications.empty()); } +namespace { +class NavigationRequestForTest : public NavigationRequest { + public: + explicit NavigationRequestForTest(base::Closure* done_closure) + : done_closure_(done_closure) {} + + ~NavigationRequestForTest() override {} + + // NavigationRequest implementation: + void StartProcessing(base::Closure done_callback) override { + *done_closure_ = std::move(done_callback); + } + + private: + base::Closure* done_closure_; // NOT OWNED +}; +} // namespace + +TEST_F(DeterministicDispatcherTest, NavigationBlocksUrlRequests) { + std::vector<std::string> notifications; + std::unique_ptr<FakeManagedDispatchURLRequestJob> job1( + new FakeManagedDispatchURLRequestJob(deterministic_dispatcher_.get(), 1, + ¬ifications)); + base::Closure navigation_done_closure; + deterministic_dispatcher_->NavigationRequested( + base::MakeUnique<NavigationRequestForTest>(&navigation_done_closure)); + std::unique_ptr<FakeManagedDispatchURLRequestJob> job2( + new FakeManagedDispatchURLRequestJob(deterministic_dispatcher_.get(), 2, + ¬ifications)); + std::unique_ptr<FakeManagedDispatchURLRequestJob> job3( + new FakeManagedDispatchURLRequestJob(deterministic_dispatcher_.get(), 3, + ¬ifications)); + std::unique_ptr<FakeManagedDispatchURLRequestJob> job4( + new FakeManagedDispatchURLRequestJob(deterministic_dispatcher_.get(), 4, + ¬ifications)); + job1->DispatchHeadersComplete(); + job2->DispatchHeadersComplete(); + job3->DispatchHeadersComplete(); + job4->DispatchHeadersComplete(); + + EXPECT_TRUE(notifications.empty()); + + base::RunLoop().RunUntilIdle(); + EXPECT_THAT(notifications, ElementsAre("id: 1 OK")); + + // This triggers a call to NavigationRequestForTest::StartProcessing. + job1.reset(); + base::RunLoop().RunUntilIdle(); + EXPECT_THAT(notifications, ElementsAre("id: 1 OK")); + + // Navigations should be blocked until we call the done closure. + navigation_done_closure.Run(); + + base::RunLoop().RunUntilIdle(); + EXPECT_THAT(notifications, ElementsAre("id: 1 OK", "id: 2 OK")); + + job2.reset(); + base::RunLoop().RunUntilIdle(); + EXPECT_THAT(notifications, ElementsAre("id: 1 OK", "id: 2 OK", "id: 3 OK")); + + job3.reset(); + base::RunLoop().RunUntilIdle(); + EXPECT_THAT(notifications, + ElementsAre("id: 1 OK", "id: 2 OK", "id: 3 OK", "id: 4 OK")); +} + } // namespace headless diff --git a/chromium/headless/public/util/deterministic_http_protocol_handler.cc b/chromium/headless/public/util/deterministic_http_protocol_handler.cc index bb2c1117e21..6a186b99cf9 100644 --- a/chromium/headless/public/util/deterministic_http_protocol_handler.cc +++ b/chromium/headless/public/util/deterministic_http_protocol_handler.cc @@ -24,6 +24,7 @@ class DeterministicHttpProtocolHandler::NopGenericURLRequestJobDelegate // GenericURLRequestJob::Delegate methods: bool BlockOrRewriteRequest( const GURL& url, + const std::string& devtools_id, const std::string& method, const std::string& referrer, GenericURLRequestJob::RewriteCallback callback) override { @@ -32,12 +33,14 @@ class DeterministicHttpProtocolHandler::NopGenericURLRequestJobDelegate const GenericURLRequestJob::HttpResponse* MaybeMatchResource( const GURL& url, + const std::string& devtools_id, const std::string& method, const net::HttpRequestHeaders& request_headers) override { return nullptr; } void OnResourceLoadComplete(const GURL& final_url, + const std::string& devtools_id, const std::string& mime_type, int http_response_code) override {} diff --git a/chromium/headless/public/util/dom_tree_extractor.h b/chromium/headless/public/util/dom_tree_extractor.h index dcd08bfe917..1ba9bf39200 100644 --- a/chromium/headless/public/util/dom_tree_extractor.h +++ b/chromium/headless/public/util/dom_tree_extractor.h @@ -19,6 +19,7 @@ class HeadlessDevToolsClient; // addition, it also extracts details of bounding boxes and layout text (NB the // exact layout should not be regarded as stable, it's subject to change without // notice). +// TODO(alexclarke): Remove in favor of one using DOM.getFlattenedDocument. class DomTreeExtractor { public: explicit DomTreeExtractor(HeadlessDevToolsClient* devtools_client); diff --git a/chromium/headless/public/util/dom_tree_extractor_browsertest.cc b/chromium/headless/public/util/dom_tree_extractor_browsertest.cc index 22b237960c5..cd1dc24d4bc 100644 --- a/chromium/headless/public/util/dom_tree_extractor_browsertest.cc +++ b/chromium/headless/public/util/dom_tree_extractor_browsertest.cc @@ -189,6 +189,7 @@ class DomTreeExtractorBrowserTest : public HeadlessAsyncDevTooledBrowserTest, "nodeName": "HTML", "nodeType": 1, "nodeValue": "", + "parentId": 1, "styleIndex": 0 })raw_string", @@ -200,7 +201,8 @@ class DomTreeExtractorBrowserTest : public HeadlessAsyncDevTooledBrowserTest, "nodeId": 3, "nodeName": "HEAD", "nodeType": 1, - "nodeValue": "" + "nodeValue": "", + "parentId": 2 })raw_string", R"raw_string({ @@ -211,7 +213,8 @@ class DomTreeExtractorBrowserTest : public HeadlessAsyncDevTooledBrowserTest, "nodeId": 4, "nodeName": "TITLE", "nodeType": 1, - "nodeValue": "" + "nodeValue": "", + "parentId": 3 })raw_string", R"raw_string({ @@ -220,7 +223,8 @@ class DomTreeExtractorBrowserTest : public HeadlessAsyncDevTooledBrowserTest, "nodeId": 5, "nodeName": "#text", "nodeType": 3, - "nodeValue": "Hello world!" + "nodeValue": "Hello world!", + "parentId": 4 })raw_string", R"raw_string({ @@ -232,7 +236,8 @@ class DomTreeExtractorBrowserTest : public HeadlessAsyncDevTooledBrowserTest, "nodeId": 6, "nodeName": "LINK", "nodeType": 1, - "nodeValue": "" + "nodeValue": "", + "parentId": 3 })raw_string", R"raw_string({ @@ -250,6 +255,7 @@ class DomTreeExtractorBrowserTest : public HeadlessAsyncDevTooledBrowserTest, "nodeName": "BODY", "nodeType": 1, "nodeValue": "", + "parentId": 2, "styleIndex": 1 })raw_string", @@ -268,6 +274,7 @@ class DomTreeExtractorBrowserTest : public HeadlessAsyncDevTooledBrowserTest, "nodeName": "DIV", "nodeType": 1, "nodeValue": "", + "parentId": 7, "styleIndex": 0 })raw_string", @@ -286,6 +293,7 @@ class DomTreeExtractorBrowserTest : public HeadlessAsyncDevTooledBrowserTest, "nodeName": "H1", "nodeType": 1, "nodeValue": "", + "parentId": 8, "styleIndex": 2 })raw_string", @@ -313,6 +321,7 @@ class DomTreeExtractorBrowserTest : public HeadlessAsyncDevTooledBrowserTest, "nodeName": "#text", "nodeType": 3, "nodeValue": "Some text.", + "parentId": 9, "styleIndex": 2 })raw_string", @@ -334,6 +343,7 @@ class DomTreeExtractorBrowserTest : public HeadlessAsyncDevTooledBrowserTest, "nodeName": "IFRAME", "nodeType": 1, "nodeValue": "", + "parentId": 8, "styleIndex": 6 })raw_string", @@ -366,6 +376,7 @@ class DomTreeExtractorBrowserTest : public HeadlessAsyncDevTooledBrowserTest, "nodeName": "HTML", "nodeType": 1, "nodeValue": "", + "parentId": 12, "styleIndex": 3 })raw_string", @@ -377,7 +388,8 @@ class DomTreeExtractorBrowserTest : public HeadlessAsyncDevTooledBrowserTest, "nodeId": 14, "nodeName": "HEAD", "nodeType": 1, - "nodeValue": "" + "nodeValue": "", + "parentId": 13 })raw_string", R"raw_string({ @@ -395,6 +407,7 @@ class DomTreeExtractorBrowserTest : public HeadlessAsyncDevTooledBrowserTest, "nodeName": "BODY", "nodeType": 1, "nodeValue": "", + "parentId": 13, "styleIndex": 4 })raw_string", @@ -413,6 +426,7 @@ class DomTreeExtractorBrowserTest : public HeadlessAsyncDevTooledBrowserTest, "nodeName": "H1", "nodeType": 1, "nodeValue": "", + "parentId": 15, "styleIndex": 5 })raw_string", @@ -440,6 +454,7 @@ class DomTreeExtractorBrowserTest : public HeadlessAsyncDevTooledBrowserTest, "nodeName": "#text", "nodeType": 3, "nodeValue": "Hello from the iframe!", + "parentId": 16, "styleIndex": 5 })raw_string", @@ -458,6 +473,7 @@ class DomTreeExtractorBrowserTest : public HeadlessAsyncDevTooledBrowserTest, "nodeName": "DIV", "nodeType": 1, "nodeValue": "", + "parentId": 8, "styleIndex": 0 })raw_string", @@ -476,6 +492,7 @@ class DomTreeExtractorBrowserTest : public HeadlessAsyncDevTooledBrowserTest, "nodeName": "DIV", "nodeType": 1, "nodeValue": "", + "parentId": 18, "styleIndex": 0 })raw_string", @@ -494,6 +511,7 @@ class DomTreeExtractorBrowserTest : public HeadlessAsyncDevTooledBrowserTest, "nodeName": "DIV", "nodeType": 1, "nodeValue": "", + "parentId": 19, "styleIndex": 0 })raw_string", @@ -512,6 +530,7 @@ class DomTreeExtractorBrowserTest : public HeadlessAsyncDevTooledBrowserTest, "nodeName": "A", "nodeType": 1, "nodeValue": "", + "parentId": 20, "styleIndex": 7 })raw_string", @@ -539,6 +558,7 @@ class DomTreeExtractorBrowserTest : public HeadlessAsyncDevTooledBrowserTest, "nodeName": "#text", "nodeType": 3, "nodeValue": "Google!", + "parentId": 21, "styleIndex": 7 })raw_string", @@ -557,6 +577,7 @@ class DomTreeExtractorBrowserTest : public HeadlessAsyncDevTooledBrowserTest, "nodeName": "P", "nodeType": 1, "nodeValue": "", + "parentId": 20, "styleIndex": 8 })raw_string", @@ -584,6 +605,7 @@ class DomTreeExtractorBrowserTest : public HeadlessAsyncDevTooledBrowserTest, "nodeName": "#text", "nodeType": 3, "nodeValue": "A paragraph!", + "parentId": 23, "styleIndex": 8 })raw_string", @@ -613,6 +635,7 @@ class DomTreeExtractorBrowserTest : public HeadlessAsyncDevTooledBrowserTest, "nodeName": "BR", "nodeType": 1, "nodeValue": "", + "parentId": 20, "styleIndex": 6 })raw_string", @@ -631,9 +654,9 @@ class DomTreeExtractorBrowserTest : public HeadlessAsyncDevTooledBrowserTest, "nodeName": "DIV", "nodeType": 1, "nodeValue": "", + "parentId": 20, "styleIndex": 9 - } - )raw_string", + })raw_string", R"raw_string({ "backendNodeId": 29, @@ -659,6 +682,7 @@ class DomTreeExtractorBrowserTest : public HeadlessAsyncDevTooledBrowserTest, "nodeName": "#text", "nodeType": 3, "nodeValue": "Some ", + "parentId": 26, "styleIndex": 9 })raw_string", @@ -677,6 +701,7 @@ class DomTreeExtractorBrowserTest : public HeadlessAsyncDevTooledBrowserTest, "nodeName": "EM", "nodeType": 1, "nodeValue": "", + "parentId": 26, "styleIndex": 10 })raw_string", @@ -704,6 +729,7 @@ class DomTreeExtractorBrowserTest : public HeadlessAsyncDevTooledBrowserTest, "nodeName": "#text", "nodeType": 3, "nodeValue": "green", + "parentId": 28, "styleIndex": 10 })raw_string", @@ -731,6 +757,7 @@ class DomTreeExtractorBrowserTest : public HeadlessAsyncDevTooledBrowserTest, "nodeName": "#text", "nodeType": 3, "nodeValue": " text...", + "parentId": 26, "styleIndex": 9 })raw_string"}; diff --git a/chromium/headless/public/util/expedited_dispatcher.cc b/chromium/headless/public/util/expedited_dispatcher.cc index 0120442dd0e..649321a7ad0 100644 --- a/chromium/headless/public/util/expedited_dispatcher.cc +++ b/chromium/headless/public/util/expedited_dispatcher.cc @@ -8,6 +8,7 @@ #include "base/bind.h" #include "headless/public/util/managed_dispatch_url_request_job.h" +#include "headless/public/util/navigation_request.h" namespace headless { @@ -36,4 +37,13 @@ void ExpeditedDispatcher::DataReady(ManagedDispatchURLRequestJob* job) { void ExpeditedDispatcher::JobDeleted(ManagedDispatchURLRequestJob*) {} +void ExpeditedDispatcher::NavigationRequested( + std::unique_ptr<NavigationRequest> navigation) { + // For the ExpeditedDispatcher we don't care when the navigation is done, + // hence the empty closure. + io_thread_task_runner_->PostTask( + FROM_HERE, base::Bind(&NavigationRequest::StartProcessing, + std::move(navigation), base::Closure())); +} + } // namespace headless diff --git a/chromium/headless/public/util/expedited_dispatcher.h b/chromium/headless/public/util/expedited_dispatcher.h index 39b8057fdfc..61062c4d51d 100644 --- a/chromium/headless/public/util/expedited_dispatcher.h +++ b/chromium/headless/public/util/expedited_dispatcher.h @@ -29,6 +29,8 @@ class ExpeditedDispatcher : public URLRequestDispatcher { void JobFailed(ManagedDispatchURLRequestJob* job, net::Error error) override; void DataReady(ManagedDispatchURLRequestJob* job) override; void JobDeleted(ManagedDispatchURLRequestJob* job) override; + void NavigationRequested( + std::unique_ptr<NavigationRequest> navigation_request) override; private: scoped_refptr<base::SingleThreadTaskRunner> io_thread_task_runner_; diff --git a/chromium/headless/public/util/expedited_dispatcher_test.cc b/chromium/headless/public/util/expedited_dispatcher_test.cc index 5ef6301ba1d..54f5249b650 100644 --- a/chromium/headless/public/util/expedited_dispatcher_test.cc +++ b/chromium/headless/public/util/expedited_dispatcher_test.cc @@ -8,8 +8,10 @@ #include <string> #include <vector> +#include "base/memory/ptr_util.h" #include "base/run_loop.h" #include "base/single_thread_task_runner.h" +#include "headless/public/util/navigation_request.h" #include "headless/public/util/testing/fake_managed_dispatch_url_request_job.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" @@ -83,4 +85,51 @@ TEST_F(ExpeditedDispatcherTest, ErrorsAndDataReadyDispatchedInCallOrder) { "id: 2 OK", "id: 1 OK")); } +namespace { +class NavigationRequestForTest : public NavigationRequest { + public: + explicit NavigationRequestForTest(base::Closure* done_closure) + : done_closure_(done_closure) {} + + ~NavigationRequestForTest() override {} + + // NavigationRequest implementation: + void StartProcessing(base::Closure done_callback) override { + *done_closure_ = std::move(done_callback); + } + + private: + base::Closure* done_closure_; // NOT OWNED +}; +} // namespace + +TEST_F(ExpeditedDispatcherTest, NavigationDoesNotBlockUrlRequests) { + std::vector<std::string> notifications; + std::unique_ptr<FakeManagedDispatchURLRequestJob> job1( + new FakeManagedDispatchURLRequestJob(expedited_dispatcher_.get(), 1, + ¬ifications)); + base::Closure navigation_done_closure; + expedited_dispatcher_->NavigationRequested( + base::MakeUnique<NavigationRequestForTest>(&navigation_done_closure)); + std::unique_ptr<FakeManagedDispatchURLRequestJob> job2( + new FakeManagedDispatchURLRequestJob(expedited_dispatcher_.get(), 2, + ¬ifications)); + std::unique_ptr<FakeManagedDispatchURLRequestJob> job3( + new FakeManagedDispatchURLRequestJob(expedited_dispatcher_.get(), 3, + ¬ifications)); + std::unique_ptr<FakeManagedDispatchURLRequestJob> job4( + new FakeManagedDispatchURLRequestJob(expedited_dispatcher_.get(), 4, + ¬ifications)); + job1->DispatchHeadersComplete(); + job2->DispatchHeadersComplete(); + job3->DispatchHeadersComplete(); + job4->DispatchHeadersComplete(); + + EXPECT_TRUE(notifications.empty()); + + base::RunLoop().RunUntilIdle(); + EXPECT_THAT(notifications, + ElementsAre("id: 1 OK", "id: 2 OK", "id: 3 OK", "id: 4 OK")); +} + } // namespace headless diff --git a/chromium/headless/public/util/flat_dom_tree_extractor.cc b/chromium/headless/public/util/flat_dom_tree_extractor.cc new file mode 100644 index 00000000000..1ac9210aabc --- /dev/null +++ b/chromium/headless/public/util/flat_dom_tree_extractor.cc @@ -0,0 +1,114 @@ +// Copyright 2017 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 "headless/public/util/flat_dom_tree_extractor.h" + +#include "base/bind.h" +#include "base/json/json_writer.h" +#include "headless/public/headless_devtools_client.h" + +namespace headless { + +FlatDomTreeExtractor::FlatDomTreeExtractor( + HeadlessDevToolsClient* devtools_client) + : work_in_progress_(false), + devtools_client_(devtools_client), + weak_factory_(this) {} + +FlatDomTreeExtractor::~FlatDomTreeExtractor() {} + +void FlatDomTreeExtractor::ExtractDomTree( + const std::vector<std::string>& css_style_whitelist, + DomResultCB callback) { + DCHECK(!work_in_progress_); + work_in_progress_ = true; + + callback_ = std::move(callback); + + devtools_client_->GetDOM()->Enable(); + devtools_client_->GetDOM()->GetFlattenedDocument( + dom::GetFlattenedDocumentParams::Builder() + .SetDepth(-1) + .SetPierce(true) + .Build(), + base::Bind(&FlatDomTreeExtractor::OnDocumentFetched, + weak_factory_.GetWeakPtr())); + + devtools_client_->GetCSS()->GetExperimental()->GetLayoutTreeAndStyles( + css::GetLayoutTreeAndStylesParams::Builder() + .SetComputedStyleWhitelist(css_style_whitelist) + .Build(), + base::Bind(&FlatDomTreeExtractor::OnLayoutTreeAndStylesFetched, + weak_factory_.GetWeakPtr())); +} + +void FlatDomTreeExtractor::OnDocumentFetched( + std::unique_ptr<dom::GetFlattenedDocumentResult> result) { + dom_tree_.document_result_ = std::move(result); + MaybeExtractDomTree(); +} + +void FlatDomTreeExtractor::OnLayoutTreeAndStylesFetched( + std::unique_ptr<css::GetLayoutTreeAndStylesResult> result) { + dom_tree_.layout_tree_and_styles_result_ = std::move(result); + MaybeExtractDomTree(); +} + +void FlatDomTreeExtractor::MaybeExtractDomTree() { + if (dom_tree_.document_result_ && dom_tree_.layout_tree_and_styles_result_) { + for (const std::unique_ptr<headless::dom::Node>& node : + *dom_tree_.document_result_->GetNodes()) { + EnumerateNodes(node.get()); + } + ExtractLayoutTreeNodes(); + ExtractComputedStyles(); + devtools_client_->GetDOM()->Disable(); + + work_in_progress_ = false; + + callback_.Run(std::move(dom_tree_)); + } +} + +void FlatDomTreeExtractor::EnumerateNodes(const dom::Node* node) { + // Allocate an index and record the node pointer. + size_t index = dom_tree_.node_id_to_index_.size(); + dom_tree_.node_id_to_index_[node->GetNodeId()] = index; + dom_tree_.dom_nodes_.push_back(node); + + if (node->HasContentDocument()) + EnumerateNodes(node->GetContentDocument()); + + DCHECK(!node->HasChildren() || node->GetChildren()->empty()); +} + +void FlatDomTreeExtractor::ExtractLayoutTreeNodes() { + dom_tree_.layout_tree_nodes_.reserve( + dom_tree_.layout_tree_and_styles_result_->GetLayoutTreeNodes()->size()); + + for (const std::unique_ptr<css::LayoutTreeNode>& layout_node : + *dom_tree_.layout_tree_and_styles_result_->GetLayoutTreeNodes()) { + std::unordered_map<NodeId, size_t>::const_iterator it = + dom_tree_.node_id_to_index_.find(layout_node->GetNodeId()); + DCHECK(it != dom_tree_.node_id_to_index_.end()); + dom_tree_.layout_tree_nodes_.push_back(layout_node.get()); + } +} + +void FlatDomTreeExtractor::ExtractComputedStyles() { + dom_tree_.computed_styles_.reserve( + dom_tree_.layout_tree_and_styles_result_->GetComputedStyles()->size()); + + for (const std::unique_ptr<css::ComputedStyle>& computed_style : + *dom_tree_.layout_tree_and_styles_result_->GetComputedStyles()) { + dom_tree_.computed_styles_.push_back(computed_style.get()); + } +} + +FlatDomTreeExtractor::DomTree::DomTree() {} +FlatDomTreeExtractor::DomTree::~DomTree() {} + +FlatDomTreeExtractor::DomTree::DomTree(DomTree&& other) = default; + +} // namespace headless diff --git a/chromium/headless/public/util/flat_dom_tree_extractor.h b/chromium/headless/public/util/flat_dom_tree_extractor.h new file mode 100644 index 00000000000..14de70d8572 --- /dev/null +++ b/chromium/headless/public/util/flat_dom_tree_extractor.h @@ -0,0 +1,89 @@ +// Copyright 2017 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. + +#ifndef HEADLESS_PUBLIC_UTIL_DOM_TREE_EXTRACTOR_H_ +#define HEADLESS_PUBLIC_UTIL_DOM_TREE_EXTRACTOR_H_ + +#include <unordered_map> +#include <vector> + +#include "base/macros.h" +#include "headless/public/devtools/domains/css.h" +#include "headless/public/devtools/domains/dom.h" + +namespace headless { +class HeadlessDevToolsClient; + +// A utility class for extracting information from the DOM via DevTools. In +// addition, it also extracts details of bounding boxes and layout text (NB the +// exact layout should not be regarded as stable, it's subject to change without +// notice). +class FlatDomTreeExtractor { + public: + explicit FlatDomTreeExtractor(HeadlessDevToolsClient* devtools_client); + ~FlatDomTreeExtractor(); + + using NodeId = int; + using Index = size_t; + + class DomTree { + public: + DomTree(); + DomTree(DomTree&& other); + ~DomTree(); + + // Flattened dom tree. The root node is always the first entry. + std::vector<const dom::Node*> dom_nodes_; + + // Map of node IDs to indexes into |dom_nodes_|. + std::unordered_map<NodeId, Index> node_id_to_index_; + + std::vector<const css::LayoutTreeNode*> layout_tree_nodes_; + + std::vector<const css::ComputedStyle*> computed_styles_; + + private: + friend class FlatDomTreeExtractor; + + // Owns the raw pointers in |dom_nodes_|. + std::unique_ptr<dom::GetFlattenedDocumentResult> document_result_; + + // Owns the raw pointers in |layout_tree_nodes_|. + std::unique_ptr<css::GetLayoutTreeAndStylesResult> + layout_tree_and_styles_result_; + + DISALLOW_COPY_AND_ASSIGN(DomTree); + }; + + using DomResultCB = base::Callback<void(DomTree)>; + + // Extracts all nodes from the DOM. This is an asynchronous operation and + // it's an error to call ExtractDom while a previous operation is in flight. + void ExtractDomTree(const std::vector<std::string>& css_style_whitelist, + DomResultCB callback); + + private: + void OnDocumentFetched( + std::unique_ptr<dom::GetFlattenedDocumentResult> result); + + void OnLayoutTreeAndStylesFetched( + std::unique_ptr<css::GetLayoutTreeAndStylesResult> result); + + void MaybeExtractDomTree(); + void EnumerateNodes(const dom::Node* node); + void ExtractLayoutTreeNodes(); + void ExtractComputedStyles(); + + DomResultCB callback_; + DomTree dom_tree_; + bool work_in_progress_; + HeadlessDevToolsClient* devtools_client_; // NOT OWNED + base::WeakPtrFactory<FlatDomTreeExtractor> weak_factory_; + + DISALLOW_COPY_AND_ASSIGN(FlatDomTreeExtractor); +}; + +} // namespace headless + +#endif // HEADLESS_PUBLIC_UTIL_DOM_TREE_EXTRACTOR_H_ diff --git a/chromium/headless/public/util/flat_dom_tree_extractor_browsertest.cc b/chromium/headless/public/util/flat_dom_tree_extractor_browsertest.cc new file mode 100644 index 00000000000..e897301cd3a --- /dev/null +++ b/chromium/headless/public/util/flat_dom_tree_extractor_browsertest.cc @@ -0,0 +1,921 @@ +// Copyright 2017 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 "headless/public/util/flat_dom_tree_extractor.h" + +#include <memory> +#include "base/json/json_reader.h" +#include "base/json/json_writer.h" +#include "base/strings/string_util.h" +#include "content/public/browser/render_widget_host_view.h" +#include "content/public/browser/web_contents.h" +#include "content/public/test/browser_test.h" +#include "headless/lib/browser/headless_web_contents_impl.h" +#include "headless/public/devtools/domains/emulation.h" +#include "headless/public/devtools/domains/network.h" +#include "headless/public/devtools/domains/page.h" +#include "headless/public/headless_browser.h" +#include "headless/public/headless_devtools_client.h" +#include "headless/public/headless_devtools_target.h" +#include "headless/test/headless_browser_test.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +namespace headless { + +namespace { + +std::string NormaliseJSON(const std::string& json) { + std::unique_ptr<base::Value> parsed_json = base::JSONReader::Read(json); + DCHECK(parsed_json); + std::string normalized_json; + base::JSONWriter::WriteWithOptions( + *parsed_json, base::JSONWriter::OPTIONS_PRETTY_PRINT, &normalized_json); + return normalized_json; +} + +} // namespace + +class FlatDomTreeExtractorBrowserTest + : public HeadlessAsyncDevTooledBrowserTest, + public page::Observer { + public: + void RunDevTooledTest() override { + EXPECT_TRUE(embedded_test_server()->Start()); + devtools_client_->GetPage()->AddObserver(this); + devtools_client_->GetPage()->Enable(); + devtools_client_->GetPage()->Navigate( + embedded_test_server()->GetURL("/dom_tree_test.html").spec()); + } + + void OnLoadEventFired(const page::LoadEventFiredParams& params) override { + devtools_client_->GetPage()->Disable(); + devtools_client_->GetPage()->RemoveObserver(this); + + extractor_.reset(new FlatDomTreeExtractor(devtools_client_.get())); + + std::vector<std::string> css_whitelist = { + "color", "display", "font-style", "font-family", + "margin-left", "margin-right", "margin-top", "margin-bottom"}; + extractor_->ExtractDomTree( + css_whitelist, + base::Bind(&FlatDomTreeExtractorBrowserTest::OnDomTreeExtracted, + base::Unretained(this))); + } + + void OnDomTreeExtracted(FlatDomTreeExtractor::DomTree dom_tree) { + GURL::Replacements replace_port; + replace_port.SetPortStr(""); + + std::vector<std::unique_ptr<base::DictionaryValue>> dom_nodes( + dom_tree.dom_nodes_.size()); + + std::map<int, std::unique_ptr<base::ListValue>> child_lists; + + // For convenience flatten the dom tree into an array. + for (size_t i = 0; i < dom_tree.dom_nodes_.size(); i++) { + dom::Node* node = const_cast<dom::Node*>(dom_tree.dom_nodes_[i]); + + dom_nodes[i].reset( + static_cast<base::DictionaryValue*>(node->Serialize().release())); + + if (node->HasParentId()) { + if (child_lists.find(node->GetParentId()) == child_lists.end()) { + child_lists.insert(std::make_pair( + node->GetParentId(), base::MakeUnique<base::ListValue>())); + } + child_lists[node->GetParentId()]->AppendInteger(i); + } + dom_nodes[i]->Remove("children", nullptr); + + // Convert content document pointers into indexes. + if (node->HasContentDocument()) { + dom_nodes[i]->SetInteger( + "contentDocumentIndex", + dom_tree + .node_id_to_index_[node->GetContentDocument()->GetNodeId()]); + dom_nodes[i]->Remove("contentDocument", nullptr); + } + + dom_nodes[i]->Remove("childNodeCount", nullptr); + + // Frame IDs are random. + if (dom_nodes[i]->HasKey("frameId")) + dom_nodes[i]->SetString("frameId", "?"); + + // Ports are random. + std::string url; + if (dom_nodes[i]->GetString("baseURL", &url)) { + dom_nodes[i]->SetString( + "baseURL", GURL(url).ReplaceComponents(replace_port).spec()); + } + + if (dom_nodes[i]->GetString("documentURL", &url)) { + dom_nodes[i]->SetString( + "documentURL", GURL(url).ReplaceComponents(replace_port).spec()); + } + } + + for (auto& pair : child_lists) { + dom_nodes[dom_tree.node_id_to_index_[pair.first]]->Set( + "childIndices", std::move(pair.second)); + } + + // Merge LayoutTreeNode data into the dictionaries. + for (const css::LayoutTreeNode* layout_node : dom_tree.layout_tree_nodes_) { + auto it = dom_tree.node_id_to_index_.find(layout_node->GetNodeId()); + ASSERT_TRUE(it != dom_tree.node_id_to_index_.end()); + + base::DictionaryValue* node_dict = dom_nodes[it->second].get(); + node_dict->Set("boundingBox", layout_node->GetBoundingBox()->Serialize()); + + if (layout_node->HasLayoutText()) + node_dict->SetString("layoutText", layout_node->GetLayoutText()); + + if (layout_node->HasStyleIndex()) + node_dict->SetInteger("styleIndex", layout_node->GetStyleIndex()); + + if (layout_node->HasInlineTextNodes()) { + std::unique_ptr<base::ListValue> inline_text_nodes( + new base::ListValue()); + for (const std::unique_ptr<css::InlineTextBox>& inline_text_box : + *layout_node->GetInlineTextNodes()) { + size_t index = inline_text_nodes->GetSize(); + inline_text_nodes->Set(index, inline_text_box->Serialize()); + } + node_dict->Set("inlineTextNodes", std::move(inline_text_nodes)); + } + } + + std::vector<std::unique_ptr<base::DictionaryValue>> computed_styles( + dom_tree.computed_styles_.size()); + + for (size_t i = 0; i < dom_tree.computed_styles_.size(); i++) { + std::unique_ptr<base::DictionaryValue> style(new base::DictionaryValue()); + for (const auto& style_property : + *dom_tree.computed_styles_[i]->GetProperties()) { + style->SetString(style_property->GetName(), style_property->GetValue()); + } + computed_styles[i] = std::move(style); + } + + const std::vector<std::string> expected_dom_nodes = { + R"raw_string({ + "backendNodeId": 7, + "localName": "", + "nodeId": 5, + "nodeName": "#text", + "nodeType": 3, + "nodeValue": "Hello world!", + "parentId": 4 + })raw_string", + + R"raw_string({ + "attributes": [ ], + "backendNodeId": 6, + "childIndices": [ 0 ], + "localName": "title", + "nodeId": 4, + "nodeName": "TITLE", + "nodeType": 1, + "nodeValue": "", + "parentId": 3 + })raw_string", + + R"raw_string({ + "attributes": [ "href", "dom_tree_test.css", "rel", "stylesheet", + "type", "text/css" ], + "backendNodeId": 8, + "localName": "link", + "nodeId": 6, + "nodeName": "LINK", + "nodeType": 1, + "nodeValue": "", + "parentId": 3 + })raw_string", + + R"raw_string({ + "attributes": [ ], + "backendNodeId": 5, + "childIndices": [ 1, 2 ], + "localName": "head", + "nodeId": 3, + "nodeName": "HEAD", + "nodeType": 1, + "nodeValue": "", + "parentId": 2 + })raw_string", + + R"raw_string({ + "backendNodeId": 12, + "boundingBox": { + "height": 32.0, + "width": 320.0, + "x": 8.0, + "y": 8.0 + }, + "inlineTextNodes": [ { + "boundingBox": { + "height": 32.0, + "width": 320.0, + "x": 8.0, + "y": 8.0 + }, + "numCharacters": 10, + "startCharacterIndex": 0 + } ], + "layoutText": "Some text.", + "localName": "", + "nodeId": 10, + "nodeName": "#text", + "nodeType": 3, + "nodeValue": "Some text.", + "parentId": 9, + "styleIndex": 2 + })raw_string", + + R"raw_string({ + "attributes": [ "class", "red" ], + "backendNodeId": 11, + "boundingBox": { + "height": 32.0, + "width": 784.0, + "x": 8.0, + "y": 8.0 + }, + "childIndices": [ 4 ], + "localName": "h1", + "nodeId": 9, + "nodeName": "H1", + "nodeType": 1, + "nodeValue": "", + "parentId": 8, + "styleIndex": 2 + })raw_string", + + R"raw_string({ + "attributes": [ ], + "backendNodeId": 16, + "localName": "head", + "nodeId": 14, + "nodeName": "HEAD", + "nodeType": 1, + "nodeValue": "", + "parentId": 13 + })raw_string", + + R"raw_string({ + "backendNodeId": 19, + "boundingBox": { + "height": 36.0, + "width": 308.0, + "x": 8.0, + "y": 8.0 + }, + "inlineTextNodes": [ { + "boundingBox": { + "height": 36.0, + "width": 307.734375, + "x": 8.0, + "y": 8.0 + }, + "numCharacters": 22, + "startCharacterIndex": 0 + } ], + "layoutText": "Hello from the iframe!", + "localName": "", + "nodeId": 17, + "nodeName": "#text", + "nodeType": 3, + "nodeValue": "Hello from the iframe!", + "parentId": 16, + "styleIndex": 5 + })raw_string", + + R"raw_string({ + "attributes": [ ], + "backendNodeId": 18, + "boundingBox": { + "height": 37.0, + "width": 384.0, + "x": 18.0, + "y": 71.0 + }, + "childIndices": [ 7 ], + "localName": "h1", + "nodeId": 16, + "nodeName": "H1", + "nodeType": 1, + "nodeValue": "", + "parentId": 15, + "styleIndex": 5 + })raw_string", + + R"raw_string({ + "attributes": [ ], + "backendNodeId": 17, + "boundingBox": { + "height": 171.0, + "width": 384.0, + "x": 18.0, + "y": 71.0 + }, + "childIndices": [ 8 ], + "localName": "body", + "nodeId": 15, + "nodeName": "BODY", + "nodeType": 1, + "nodeValue": "", + "parentId": 13, + "styleIndex": 4 + })raw_string", + + R"raw_string({ + "attributes": [ ], + "backendNodeId": 15, + "boundingBox": { + "height": 200.0, + "width": 400.0, + "x": 10.0, + "y": 63.0 + }, + "childIndices": [ 6, 9 ], + "frameId": "?", + "localName": "html", + "nodeId": 13, + "nodeName": "HTML", + "nodeType": 1, + "nodeValue": "", + "parentId": 12, + "styleIndex": 3 + })raw_string", + + R"raw_string({ + "attributes": [ "src", "/iframe.html", "width", "400", "height", + "200" ], + "backendNodeId": 13, + "boundingBox": { + "height": 205.0, + "width": 404.0, + "x": 8.0, + "y": 61.0 + }, + "contentDocumentIndex": 12, + "frameId": "?", + "localName": "iframe", + "nodeId": 11, + "nodeName": "IFRAME", + "nodeType": 1, + "nodeValue": "", + "parentId": 8, + "styleIndex": 6 + })raw_string", + + R"raw_string({ + "backendNodeId": 14, + "baseURL": "http://127.0.0.1/iframe.html", + "childIndices": [ 10 ], + "documentURL": "http://127.0.0.1/iframe.html", + "localName": "", + "nodeId": 12, + "nodeName": "#document", + "nodeType": 9, + "nodeValue": "", + "xmlVersion": "" + })raw_string", + + R"raw_string({ + "backendNodeId": 24, + "boundingBox": { + "height": 17.0, + "width": 112.0, + "x": 8.0, + "y": 265.0 + }, + "inlineTextNodes": [ { + "boundingBox": { + "height": 16.0, + "width": 112.0, + "x": 8.0, + "y": 265.4375 + }, + "numCharacters": 7, + "startCharacterIndex": 0 + } ], + "layoutText": "Google!", + "localName": "", + "nodeId": 22, + "nodeName": "#text", + "nodeType": 3, + "nodeValue": "Google!", + "parentId": 21, + "styleIndex": 7 + })raw_string", + + R"raw_string({ + "attributes": [ "href", "https://www.google.com" ], + "backendNodeId": 23, + "boundingBox": { + "height": 17.0, + "width": 112.0, + "x": 8.0, + "y": 265.0 + }, + "childIndices": [ 13 ], + "localName": "a", + "nodeId": 21, + "nodeName": "A", + "nodeType": 1, + "nodeValue": "", + "parentId": 20, + "styleIndex": 7 + })raw_string", + + R"raw_string({ + "backendNodeId": 26, + "boundingBox": { + "height": 17.0, + "width": 192.0, + "x": 8.0, + "y": 297.0 + }, + "inlineTextNodes": [ { + "boundingBox": { + "height": 16.0, + "width": 192.0, + "x": 8.0, + "y": 297.4375 + }, + "numCharacters": 12, + "startCharacterIndex": 0 + } ], + "layoutText": "A paragraph!", + "localName": "", + "nodeId": 24, + "nodeName": "#text", + "nodeType": 3, + "nodeValue": "A paragraph!", + "parentId": 23, + "styleIndex": 8 + })raw_string", + + R"raw_string({ + "attributes": [ ], + "backendNodeId": 25, + "boundingBox": { + "height": 17.0, + "width": 784.0, + "x": 8.0, + "y": 297.0 + }, + "childIndices": [ 15 ], + "localName": "p", + "nodeId": 23, + "nodeName": "P", + "nodeType": 1, + "nodeValue": "", + "parentId": 20, + "styleIndex": 8 + })raw_string", + + R"raw_string({ + "attributes": [ ], + "backendNodeId": 27, + "boundingBox": { + "height": 0.0, + "width": 0.0, + "x": 0.0, + "y": 0.0 + }, + "inlineTextNodes": [ { + "boundingBox": { + "height": 16.0, + "width": 0.0, + "x": 8.0, + "y": 329.4375 + }, + "numCharacters": 1, + "startCharacterIndex": 0 + } ], + "layoutText": "\n", + "localName": "br", + "nodeId": 25, + "nodeName": "BR", + "nodeType": 1, + "nodeValue": "", + "parentId": 20, + "styleIndex": 6 + })raw_string", + + R"raw_string({ + "backendNodeId": 29, + "boundingBox": { + "height": 17.0, + "width": 80.0, + "x": 8.0, + "y": 345.0 + }, + "inlineTextNodes": [ { + "boundingBox": { + "height": 16.0, + "width": 80.0, + "x": 8.0, + "y": 345.4375 + }, + "numCharacters": 5, + "startCharacterIndex": 0 + } ], + "layoutText": "Some ", + "localName": "", + "nodeId": 27, + "nodeName": "#text", + "nodeType": 3, + "nodeValue": "Some ", + "parentId": 26, + "styleIndex": 9 + })raw_string", + + R"raw_string({ + "backendNodeId": 31, + "boundingBox": { + "height": 17.0, + "width": 80.0, + "x": 88.0, + "y": 345.0 + }, + "inlineTextNodes": [ { + "boundingBox": { + "height": 16.0, + "width": 80.0, + "x": 88.0, + "y": 345.4375 + }, + "numCharacters": 5, + "startCharacterIndex": 0 + } ], + "layoutText": "green", + "localName": "", + "nodeId": 29, + "nodeName": "#text", + "nodeType": 3, + "nodeValue": "green", + "parentId": 28, + "styleIndex": 10 + })raw_string", + + R"raw_string({ + "attributes": [ ], + "backendNodeId": 30, + "boundingBox": { + "height": 17.0, + "width": 80.0, + "x": 88.0, + "y": 345.0 + }, + "childIndices": [ 19 ], + "localName": "em", + "nodeId": 28, + "nodeName": "EM", + "nodeType": 1, + "nodeValue": "", + "parentId": 26, + "styleIndex": 10 + })raw_string", + + R"raw_string({ + "backendNodeId": 32, + "boundingBox": { + "height": 17.0, + "width": 128.0, + "x": 168.0, + "y": 345.0 + }, + "inlineTextNodes": [ { + "boundingBox": { + "height": 16.0, + "width": 128.0, + "x": 168.0, + "y": 345.4375 + }, + "numCharacters": 8, + "startCharacterIndex": 0 + } ], + "layoutText": " text...", + "localName": "", + "nodeId": 30, + "nodeName": "#text", + "nodeType": 3, + "nodeValue": " text...", + "parentId": 26, + "styleIndex": 9 + })raw_string", + + R"raw_string({ + "attributes": [ "class", "green" ], + "backendNodeId": 28, + "boundingBox": { + "height": 17.0, + "width": 784.0, + "x": 8.0, + "y": 345.0 + }, + "childIndices": [ 18, 20, 21 ], + "localName": "div", + "nodeId": 26, + "nodeName": "DIV", + "nodeType": 1, + "nodeValue": "", + "parentId": 20, + "styleIndex": 9 + })raw_string", + + R"raw_string({ + "attributes": [ "id", "id4" ], + "backendNodeId": 22, + "boundingBox": { + "height": 97.0, + "width": 784.0, + "x": 8.0, + "y": 265.0 + }, + "childIndices": [ 14, 16, 17, 22 ], + "localName": "div", + "nodeId": 20, + "nodeName": "DIV", + "nodeType": 1, + "nodeValue": "", + "parentId": 19, + "styleIndex": 0 + })raw_string", + + R"raw_string({ + "attributes": [ "id", "id3" ], + "backendNodeId": 21, + "boundingBox": { + "height": 97.0, + "width": 784.0, + "x": 8.0, + "y": 265.0 + }, + "childIndices": [ 23 ], + "localName": "div", + "nodeId": 19, + "nodeName": "DIV", + "nodeType": 1, + "nodeValue": "", + "parentId": 18, + "styleIndex": 0 + })raw_string", + + R"raw_string({ + "attributes": [ "id", "id2" ], + "backendNodeId": 20, + "boundingBox": { + "height": 97.0, + "width": 784.0, + "x": 8.0, + "y": 265.0 + }, + "childIndices": [ 24 ], + "localName": "div", + "nodeId": 18, + "nodeName": "DIV", + "nodeType": 1, + "nodeValue": "", + "parentId": 8, + "styleIndex": 0 + })raw_string", + + R"raw_string({ + "attributes": [ "id", "id1" ], + "backendNodeId": 10, + "boundingBox": { + "height": 354.0, + "width": 784.0, + "x": 8.0, + "y": 8.0 + }, + "childIndices": [ 5, 11, 25 ], + "localName": "div", + "nodeId": 8, + "nodeName": "DIV", + "nodeType": 1, + "nodeValue": "", + "parentId": 7, + "styleIndex": 0 + })raw_string", + + R"raw_string({ + "attributes": [ ], + "backendNodeId": 9, + "boundingBox": { + "height": 584.0, + "width": 784.0, + "x": 8.0, + "y": 8.0 + }, + "childIndices": [ 26 ], + "localName": "body", + "nodeId": 7, + "nodeName": "BODY", + "nodeType": 1, + "nodeValue": "", + "parentId": 2, + "styleIndex": 1 + })raw_string", + + R"raw_string({ + "attributes": [ ], + "backendNodeId": 4, + "boundingBox": { + "height": 600.0, + "width": 800.0, + "x": 0.0, + "y": 0.0 + }, + "childIndices": [ 3, 27 ], + "frameId": "?", + "localName": "html", + "nodeId": 2, + "nodeName": "HTML", + "nodeType": 1, + "nodeValue": "", + "parentId": 1, + "styleIndex": 0 + })raw_string", + + R"raw_string({ + "backendNodeId": 3, + "baseURL": "http://127.0.0.1/dom_tree_test.html", + "boundingBox": { + "height": 600.0, + "width": 800.0, + "x": 0.0, + "y": 0.0 + }, + "childIndices": [ 28 ], + "documentURL": "http://127.0.0.1/dom_tree_test.html", + "localName": "", + "nodeId": 1, + "nodeName": "#document", + "nodeType": 9, + "nodeValue": "", + "xmlVersion": "" + })raw_string"}; + + EXPECT_EQ(expected_dom_nodes.size(), dom_nodes.size()); + + for (size_t i = 0; i < dom_nodes.size(); i++) { + std::string result_json; + base::JSONWriter::WriteWithOptions( + *dom_nodes[i], base::JSONWriter::OPTIONS_PRETTY_PRINT, &result_json); + + ASSERT_LT(i, expected_dom_nodes.size()); + EXPECT_EQ(NormaliseJSON(expected_dom_nodes[i]), result_json) << " Node # " + << i; + } + + const std::vector<std::string> expected_styles = { + R"raw_string({ + "color": "rgb(0, 0, 0)", + "display": "block", + "font-family": "ahem", + "font-style": "normal", + "margin-bottom": "0px", + "margin-left": "0px", + "margin-right": "0px", + "margin-top": "0px" + })raw_string", + + R"raw_string({ + "color": "rgb(0, 0, 0)", + "display": "block", + "font-family": "ahem", + "font-style": "normal", + "margin-bottom": "8px", + "margin-left": "8px", + "margin-right": "8px", + "margin-top": "8px" + })raw_string", + + R"raw_string({ + "color": "rgb(255, 0, 0)", + "display": "block", + "font-family": "ahem", + "font-style": "normal", + "margin-bottom": "21.44px", + "margin-left": "0px", + "margin-right": "0px", + "margin-top": "21.44px" + })raw_string", + + R"raw_string({ + "color": "rgb(0, 0, 0)", + "display": "block", + "font-family": "\"Times New Roman\"", + "font-style": "normal", + "margin-bottom": "0px", + "margin-left": "0px", + "margin-right": "0px", + "margin-top": "0px" + })raw_string", + + R"raw_string({ + "color": "rgb(0, 0, 0)", + "display": "block", + "font-family": "\"Times New Roman\"", + "font-style": "normal", + "margin-bottom": "8px", + "margin-left": "8px", + "margin-right": "8px", + "margin-top": "8px" + })raw_string", + + R"raw_string({ + "color": "rgb(0, 0, 0)", + "display": "block", + "font-family": "\"Times New Roman\"", + "font-style": "normal", + "margin-bottom": "21.44px", + "margin-left": "0px", + "margin-right": "0px", + "margin-top": "21.44px" + })raw_string", + + R"raw_string({ + "color": "rgb(0, 0, 0)", + "display": "inline", + "font-family": "ahem", + "font-style": "normal", + "margin-bottom": "0px", + "margin-left": "0px", + "margin-right": "0px", + "margin-top": "0px" + })raw_string", + + R"raw_string({ + "color": "rgb(0, 0, 238)", + "display": "inline", + "font-family": "ahem", + "font-style": "normal", + "margin-bottom": "0px", + "margin-left": "0px", + "margin-right": "0px", + "margin-top": "0px" + })raw_string", + + R"raw_string({ + "color": "rgb(0, 0, 0)", + "display": "block", + "font-family": "ahem", + "font-style": "normal", + "margin-bottom": "16px", + "margin-left": "0px", + "margin-right": "0px", + "margin-top": "16px" + })raw_string", + + R"raw_string({ + "color": "rgb(0, 128, 0)", + "display": "block", + "font-family": "ahem", + "font-style": "normal", + "margin-bottom": "0px", + "margin-left": "0px", + "margin-right": "0px", + "margin-top": "0px" + })raw_string", + + R"raw_string({ + "color": "rgb(0, 128, 0)", + "display": "inline", + "font-family": "ahem", + "font-style": "italic", + "margin-bottom": "0px", + "margin-left": "0px", + "margin-right": "0px", + "margin-top": "0px" + } + )raw_string"}; + + for (size_t i = 0; i < computed_styles.size(); i++) { + std::string result_json; + base::JSONWriter::WriteWithOptions(*computed_styles[i], + base::JSONWriter::OPTIONS_PRETTY_PRINT, + &result_json); + + ASSERT_LT(i, expected_styles.size()); + EXPECT_EQ(NormaliseJSON(expected_styles[i]), result_json) << " Style # " + << i; + } + + FinishAsynchronousTest(); + } + + std::unique_ptr<FlatDomTreeExtractor> extractor_; +}; + +HEADLESS_ASYNC_DEVTOOLED_TEST_F(FlatDomTreeExtractorBrowserTest); + +} // namespace headless diff --git a/chromium/headless/public/util/generic_url_request_job.cc b/chromium/headless/public/util/generic_url_request_job.cc index a82925d9623..7918dec7836 100644 --- a/chromium/headless/public/util/generic_url_request_job.cc +++ b/chromium/headless/public/util/generic_url_request_job.cc @@ -25,6 +25,9 @@ bool IsMethodSafe(const std::string& method) { method == "TRACE"; } +// Keep in sync with X_DevTools_Request_Id defined in HTTPNames.json5. +const char kDevtoolsRequestId[] = "X-DevTools-Request-Id"; + } // namespace GenericURLRequestJob::GenericURLRequestJob( @@ -45,6 +48,11 @@ GenericURLRequestJob::~GenericURLRequestJob() = default; void GenericURLRequestJob::SetExtraRequestHeaders( const net::HttpRequestHeaders& headers) { extra_request_headers_ = headers; + + if (extra_request_headers_.GetHeader(kDevtoolsRequestId, + &devtools_request_id_)) { + extra_request_headers_.RemoveHeader(kDevtoolsRequestId); + } } void GenericURLRequestJob::Start() { @@ -67,7 +75,8 @@ void GenericURLRequestJob::Start() { } }; - if (!delegate_->BlockOrRewriteRequest(request_->url(), request_->method(), + if (!delegate_->BlockOrRewriteRequest(request_->url(), devtools_request_id_, + request_->method(), request_->referrer(), callback)) { PrepareCookies(request_->url(), request_->method(), url::Origin(request_->first_party_for_cookies())); @@ -118,7 +127,7 @@ void GenericURLRequestJob::OnCookiesAvailable( // The resource may have been supplied in the request. const HttpResponse* matched_resource = delegate_->MaybeMatchResource( - rewritten_url, method, extra_request_headers_); + rewritten_url, devtools_request_id_, method, extra_request_headers_); if (matched_resource) { OnFetchCompleteExtractHeaders( @@ -151,7 +160,8 @@ void GenericURLRequestJob::OnFetchComplete( std::string mime_type; GetMimeType(&mime_type); - delegate_->OnResourceLoadComplete(final_url, mime_type, http_response_code); + delegate_->OnResourceLoadComplete(final_url, devtools_request_id_, mime_type, + http_response_code); } int GenericURLRequestJob::ReadRawData(net::IOBuffer* buf, int buf_size) { @@ -193,4 +203,46 @@ void GenericURLRequestJob::GetLoadTimingInfo( load_timing_info->receive_headers_end = response_time_; } +bool GenericURLRequestJob::Delegate::BlockOrRewriteRequest( + const GURL& url, + const std::string& devtools_id, + const std::string& method, + const std::string& referrer, + RewriteCallback callback) { + return BlockOrRewriteRequest(url, method, referrer, callback); +} + +bool GenericURLRequestJob::Delegate::BlockOrRewriteRequest( + const GURL& url, + const std::string& method, + const std::string& referrer, + RewriteCallback callback) { + return false; +} + +const GenericURLRequestJob::HttpResponse* +GenericURLRequestJob::Delegate::MaybeMatchResource( + const GURL& url, + const std::string& devtools_id, + const std::string& method, + const net::HttpRequestHeaders& request_headers) { + return MaybeMatchResource(url, method, request_headers); +} + +const GenericURLRequestJob::HttpResponse* +GenericURLRequestJob::Delegate::MaybeMatchResource( + const GURL& url, + const std::string& method, + const net::HttpRequestHeaders& request_headers) { + return nullptr; +} + +void GenericURLRequestJob::Delegate::OnResourceLoadComplete( + const GURL& final_url, + const std::string& devtools_id, + const std::string& mime_type, + int http_response_code) { + OnResourceLoadComplete(final_url, mime_type, http_response_code); +} + } // namespace headless diff --git a/chromium/headless/public/util/generic_url_request_job.h b/chromium/headless/public/util/generic_url_request_job.h index 850f0ce6c99..1bb08905531 100644 --- a/chromium/headless/public/util/generic_url_request_job.h +++ b/chromium/headless/public/util/generic_url_request_job.h @@ -58,21 +58,38 @@ class GenericURLRequestJob : public ManagedDispatchURLRequestJob, // with the result, or false to indicate that no rewriting is necessary. // Called on an arbitrary thread. virtual bool BlockOrRewriteRequest(const GURL& url, + const std::string& devtools_id, const std::string& method, const std::string& referrer, - RewriteCallback callback) = 0; + RewriteCallback callback); + // TODO(alexclarke): Make the above pure virtual and remove this. + virtual bool BlockOrRewriteRequest(const GURL& url, + const std::string& method, + const std::string& referrer, + RewriteCallback callback); // Allows the delegate to synchronously fulfill a request with a reply. // Called on an arbitrary thread. virtual const HttpResponse* MaybeMatchResource( const GURL& url, + const std::string& devtools_id, + const std::string& method, + const net::HttpRequestHeaders& request_headers); + // TODO(alexclarke): Make the above pure virtual and remove this. + virtual const HttpResponse* MaybeMatchResource( + const GURL& url, const std::string& method, - const net::HttpRequestHeaders& request_headers) = 0; + const net::HttpRequestHeaders& request_headers); // Signals that a resource load has finished. Called on an arbitrary thread. virtual void OnResourceLoadComplete(const GURL& final_url, + const std::string& devtools_id, + const std::string& mime_type, + int http_response_code); + // TODO(alexclarke): Make the above pure virtual and remove this. + virtual void OnResourceLoadComplete(const GURL& final_url, const std::string& mime_type, - int http_response_code) = 0; + int http_response_code) {} protected: virtual ~Delegate() {} @@ -117,6 +134,7 @@ class GenericURLRequestJob : public ManagedDispatchURLRequestJob, std::unique_ptr<URLFetcher> url_fetcher_; net::HttpRequestHeaders extra_request_headers_; scoped_refptr<net::HttpResponseHeaders> response_headers_; + std::string devtools_request_id_; Delegate* delegate_; // Not owned. const char* body_ = nullptr; // Not owned. int http_response_code_ = 0; diff --git a/chromium/headless/public/util/generic_url_request_job_test.cc b/chromium/headless/public/util/generic_url_request_job_test.cc index e1942f86e8b..79dc9e2bf52 100644 --- a/chromium/headless/public/util/generic_url_request_job_test.cc +++ b/chromium/headless/public/util/generic_url_request_job_test.cc @@ -281,7 +281,7 @@ TEST_F(GenericURLRequestJobTest, RequestWithCookies) { base::Time(), base::Time(), /* secure */ false, /* http_only */ false, net::CookieSameSite::NO_RESTRICTION, - /* enforce_strict_secure */ false, net::COOKIE_PRIORITY_DEFAULT)); + net::COOKIE_PRIORITY_DEFAULT)); // Matching secure cookie. cookies->push_back(*net::CanonicalCookie::Create( @@ -289,7 +289,7 @@ TEST_F(GenericURLRequestJobTest, RequestWithCookies) { base::Time(), base::Time(), /* secure */ true, /* http_only */ false, net::CookieSameSite::NO_RESTRICTION, - /* enforce_strict_secure */ true, net::COOKIE_PRIORITY_DEFAULT)); + net::COOKIE_PRIORITY_DEFAULT)); // Matching http-only cookie. cookies->push_back(*net::CanonicalCookie::Create( @@ -297,7 +297,7 @@ TEST_F(GenericURLRequestJobTest, RequestWithCookies) { base::Time(), base::Time(), /* secure */ false, /* http_only */ true, net::CookieSameSite::NO_RESTRICTION, - /* enforce_strict_secure */ false, net::COOKIE_PRIORITY_DEFAULT)); + net::COOKIE_PRIORITY_DEFAULT)); // Matching cookie with path. cookies->push_back(*net::CanonicalCookie::Create( @@ -305,7 +305,7 @@ TEST_F(GenericURLRequestJobTest, RequestWithCookies) { "/widgets", base::Time(), base::Time(), /* secure */ false, /* http_only */ false, net::CookieSameSite::NO_RESTRICTION, - /* enforce_strict_secure */ false, net::COOKIE_PRIORITY_DEFAULT)); + net::COOKIE_PRIORITY_DEFAULT)); // Matching cookie with subdomain. cookies->push_back(*net::CanonicalCookie::Create( @@ -313,7 +313,7 @@ TEST_F(GenericURLRequestJobTest, RequestWithCookies) { "cdn.example.com", "/", base::Time(), base::Time(), /* secure */ false, /* http_only */ false, net::CookieSameSite::NO_RESTRICTION, - /* enforce_strict_secure */ false, net::COOKIE_PRIORITY_DEFAULT)); + net::COOKIE_PRIORITY_DEFAULT)); // Non-matching cookie (different site). cookies->push_back(*net::CanonicalCookie::Create( @@ -321,7 +321,7 @@ TEST_F(GenericURLRequestJobTest, RequestWithCookies) { base::Time(), base::Time(), /* secure */ false, /* http_only */ false, net::CookieSameSite::NO_RESTRICTION, - /* enforce_strict_secure */ false, net::COOKIE_PRIORITY_DEFAULT)); + net::COOKIE_PRIORITY_DEFAULT)); // Non-matching cookie (different path). cookies->push_back(*net::CanonicalCookie::Create( @@ -329,7 +329,7 @@ TEST_F(GenericURLRequestJobTest, RequestWithCookies) { "/gadgets", base::Time(), base::Time(), /* secure */ false, /* http_only */ false, net::CookieSameSite::NO_RESTRICTION, - /* enforce_strict_secure */ false, net::COOKIE_PRIORITY_DEFAULT)); + net::COOKIE_PRIORITY_DEFAULT)); std::string reply = "{\"url\":\"https://example.com\"," diff --git a/chromium/headless/public/util/navigation_request.h b/chromium/headless/public/util/navigation_request.h new file mode 100644 index 00000000000..cd28f10dd93 --- /dev/null +++ b/chromium/headless/public/util/navigation_request.h @@ -0,0 +1,31 @@ +// Copyright 2017 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. + +#ifndef HEADLESS_PUBLIC_UTIL_NAVIGATION_REQUEST_H_ +#define HEADLESS_PUBLIC_UTIL_NAVIGATION_REQUEST_H_ + +#include "base/macros.h" + +namespace headless { + +// While the actual details of the navigation processing are left undefined, +// it's anticipated implementations will use devtools Page.setControlNavigations +// and Page.processNavigation commands. +class NavigationRequest { + public: + NavigationRequest() {} + virtual ~NavigationRequest() {} + + // Called on the IO thread to ask the implementation to start processing the + // navigation request. The NavigationRequest will be deleted immediately after + // The |done_callback| can be called from any thread. + virtual void StartProcessing(base::Closure done_callback) = 0; + + private: + DISALLOW_COPY_AND_ASSIGN(NavigationRequest); +}; + +} // namespace headless + +#endif // HEADLESS_PUBLIC_UTIL_NAVIGATION_REQUEST_H_ diff --git a/chromium/headless/public/util/protocol_handler_request_id_browsertest.cc b/chromium/headless/public/util/protocol_handler_request_id_browsertest.cc new file mode 100644 index 00000000000..986bf1f1298 --- /dev/null +++ b/chromium/headless/public/util/protocol_handler_request_id_browsertest.cc @@ -0,0 +1,218 @@ +// Copyright 2017 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 "content/public/test/browser_test.h" +#include "headless/public/devtools/domains/network.h" +#include "headless/public/devtools/domains/page.h" +#include "headless/public/headless_devtools_client.h" +#include "headless/public/util/expedited_dispatcher.h" +#include "headless/public/util/generic_url_request_job.h" +#include "headless/public/util/url_fetcher.h" +#include "headless/test/headless_browser_test.h" +#include "net/url_request/url_request_job_factory.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +using testing::ContainerEq; + +namespace headless { + +namespace { +// Keep in sync with X_DevTools_Request_Id defined in HTTPNames.json5. +const char kDevtoolsRequestId[] = "X-DevTools-Request-Id"; +} // namespace + +namespace { +class RequestIdCorrelationProtocolHandler + : public net::URLRequestJobFactory::ProtocolHandler { + public: + explicit RequestIdCorrelationProtocolHandler( + scoped_refptr<base::SingleThreadTaskRunner> io_thread_task_runner) + : test_delegate_(new TestDelegate(this)), + dispatcher_(new ExpeditedDispatcher(io_thread_task_runner)) {} + + ~RequestIdCorrelationProtocolHandler() override {} + + struct Response { + Response() {} + Response(const std::string& body, const std::string& mime_type) + : data("HTTP/1.1 200 OK\r\nContent-Type: " + mime_type + "\r\n\r\n" + + body) {} + + std::string data; + }; + + void InsertResponse(const std::string& url, const Response& response) { + response_map_[url] = response; + } + + const Response* GetResponse(const std::string& url) const { + std::map<std::string, Response>::const_iterator find_it = + response_map_.find(url); + if (find_it == response_map_.end()) + return nullptr; + return &find_it->second; + } + + class MockURLFetcher : public URLFetcher { + public: + explicit MockURLFetcher( + const RequestIdCorrelationProtocolHandler* protocol_handler) + : protocol_handler_(protocol_handler) {} + ~MockURLFetcher() override {} + + // URLFetcher implementation: + void StartFetch(const GURL& url, + const std::string& method, + const net::HttpRequestHeaders& request_headers, + ResultListener* result_listener) override { + const Response* response = protocol_handler_->GetResponse(url.spec()); + if (!response) + result_listener->OnFetchStartError(net::ERR_FILE_NOT_FOUND); + + // The header used for correlation should not be sent to the fetcher. + EXPECT_FALSE(request_headers.HasHeader(kDevtoolsRequestId)); + + result_listener->OnFetchCompleteExtractHeaders( + url, 200, response->data.c_str(), response->data.size()); + } + + private: + const RequestIdCorrelationProtocolHandler* protocol_handler_; + + DISALLOW_COPY_AND_ASSIGN(MockURLFetcher); + }; + + class TestDelegate : public GenericURLRequestJob::Delegate { + public: + explicit TestDelegate(RequestIdCorrelationProtocolHandler* protocol_handler) + : protocol_handler_(protocol_handler) {} + ~TestDelegate() override {} + + // GenericURLRequestJob::Delegate implementation: + bool BlockOrRewriteRequest( + const GURL& url, + const std::string& devtools_id, + const std::string& method, + const std::string& referrer, + GenericURLRequestJob::RewriteCallback callback) override { + protocol_handler_->url_to_devtools_id_[url.spec()] = devtools_id; + return false; + } + + const GenericURLRequestJob::HttpResponse* MaybeMatchResource( + const GURL& url, + const std::string& devtools_id, + const std::string& method, + const net::HttpRequestHeaders& request_headers) override { + return nullptr; + } + + void OnResourceLoadComplete(const GURL& final_url, + const std::string& devtools_id, + const std::string& mime_type, + int http_response_code) override {} + + private: + RequestIdCorrelationProtocolHandler* protocol_handler_; + + DISALLOW_COPY_AND_ASSIGN(TestDelegate); + }; + + // net::URLRequestJobFactory::ProtocolHandler implementation:: + net::URLRequestJob* MaybeCreateJob( + net::URLRequest* request, + net::NetworkDelegate* network_delegate) const override { + return new GenericURLRequestJob( + request, network_delegate, dispatcher_.get(), + base::MakeUnique<MockURLFetcher>(this), test_delegate_.get()); + } + + std::map<std::string, std::string> url_to_devtools_id_; + + private: + std::unique_ptr<TestDelegate> test_delegate_; + std::unique_ptr<ExpeditedDispatcher> dispatcher_; + std::map<std::string, Response> response_map_; + + DISALLOW_COPY_AND_ASSIGN(RequestIdCorrelationProtocolHandler); +}; + +const char* kIndexHtml = R"( +<html> +<head> +<link rel="stylesheet" type="text/css" href="style1.css"> +<link rel="stylesheet" type="text/css" href="style2.css"> +</head> +<body>Hello. +</body> +</html>)"; + +const char* kStyle1 = R"( +.border { + border: 1px solid #000; +})"; + +const char* kStyle2 = R"( +.border { + border: 2px solid #fff; +})"; + +} // namespace + +class ProtocolHandlerRequestIdCorrelationTest + : public HeadlessAsyncDevTooledBrowserTest, + public network::Observer, + public page::Observer { + public: + void RunDevTooledTest() override { + EXPECT_TRUE(embedded_test_server()->Start()); + devtools_client_->GetPage()->AddObserver(this); + devtools_client_->GetPage()->Enable(); + devtools_client_->GetNetwork()->AddObserver(this); + devtools_client_->GetNetwork()->Enable(); + devtools_client_->GetPage()->Navigate("http://foo.com/index.html"); + } + + ProtocolHandlerMap GetProtocolHandlers() override { + ProtocolHandlerMap protocol_handlers; + std::unique_ptr<RequestIdCorrelationProtocolHandler> http_handler( + new RequestIdCorrelationProtocolHandler(browser()->BrowserIOThread())); + http_handler_ = http_handler.get(); + http_handler_->InsertResponse("http://foo.com/index.html", + {kIndexHtml, "text/html"}); + http_handler_->InsertResponse("http://foo.com/style1.css", + {kStyle1, "text/css"}); + http_handler_->InsertResponse("http://foo.com/style2.css", + {kStyle2, "text/css"}); + protocol_handlers[url::kHttpScheme] = std::move(http_handler); + return protocol_handlers; + } + + // network::Observer implementation: + void OnRequestWillBeSent( + const network::RequestWillBeSentParams& params) override { + url_to_devtools_id_[params.GetRequest()->GetUrl()] = params.GetRequestId(); + EXPECT_FALSE(params.GetRequest()->GetHeaders()->HasKey(kDevtoolsRequestId)); + } + + // page::Observer implementation: + void OnLoadEventFired(const page::LoadEventFiredParams& params) override { + // Make sure that our protocol handler saw the same url : devtools ids as + // our OnRequestWillBeSent event listener did. + EXPECT_THAT(url_to_devtools_id_, + ContainerEq(http_handler_->url_to_devtools_id_)); + EXPECT_EQ(3u, url_to_devtools_id_.size()); + FinishAsynchronousTest(); + } + + private: + std::map<std::string, std::string> url_to_devtools_id_; + RequestIdCorrelationProtocolHandler* http_handler_; // NOT OWNED +}; + +HEADLESS_ASYNC_DEVTOOLED_TEST_F(ProtocolHandlerRequestIdCorrelationTest); + +} // namespace headless diff --git a/chromium/headless/public/util/testing/generic_url_request_mocks.cc b/chromium/headless/public/util/testing/generic_url_request_mocks.cc index adcabbef839..54e6c67c54a 100644 --- a/chromium/headless/public/util/testing/generic_url_request_mocks.cc +++ b/chromium/headless/public/util/testing/generic_url_request_mocks.cc @@ -20,6 +20,7 @@ MockGenericURLRequestJobDelegate::~MockGenericURLRequestJobDelegate() {} bool MockGenericURLRequestJobDelegate::BlockOrRewriteRequest( const GURL& url, + const std::string& devtools_id, const std::string& method, const std::string& referrer, GenericURLRequestJob::RewriteCallback callback) { @@ -31,6 +32,7 @@ bool MockGenericURLRequestJobDelegate::BlockOrRewriteRequest( const GenericURLRequestJob::HttpResponse* MockGenericURLRequestJobDelegate::MaybeMatchResource( const GURL& url, + const std::string& devtools_id, const std::string& method, const net::HttpRequestHeaders& request_headers) { return nullptr; @@ -38,6 +40,7 @@ MockGenericURLRequestJobDelegate::MaybeMatchResource( void MockGenericURLRequestJobDelegate::OnResourceLoadComplete( const GURL& final_url, + const std::string& devtools_id, const std::string& mime_type, int http_response_code) {} @@ -65,7 +68,6 @@ void MockCookieStore::SetCookieWithDetailsAsync( bool secure, bool http_only, net::CookieSameSite same_site, - bool enforce_strict_secure, net::CookiePriority priority, const SetCookiesCallback& callback) { CHECK(false); diff --git a/chromium/headless/public/util/testing/generic_url_request_mocks.h b/chromium/headless/public/util/testing/generic_url_request_mocks.h index fb8f0bef954..8483e569139 100644 --- a/chromium/headless/public/util/testing/generic_url_request_mocks.h +++ b/chromium/headless/public/util/testing/generic_url_request_mocks.h @@ -25,16 +25,19 @@ class MockGenericURLRequestJobDelegate : public GenericURLRequestJob::Delegate { bool BlockOrRewriteRequest( const GURL& url, + const std::string& devtools_id, const std::string& method, const std::string& referrer, GenericURLRequestJob::RewriteCallback callback) override; const GenericURLRequestJob::HttpResponse* MaybeMatchResource( const GURL& url, + const std::string& devtools_id, const std::string& method, const net::HttpRequestHeaders& request_headers) override; void OnResourceLoadComplete(const GURL& final_url, + const std::string& devtools_id, const std::string& mime_type, int http_response_code) override; @@ -69,7 +72,6 @@ class MockCookieStore : public net::CookieStore { bool secure, bool http_only, net::CookieSameSite same_site, - bool enforce_strict_secure, net::CookiePriority priority, const SetCookiesCallback& callback) override; diff --git a/chromium/headless/public/util/url_request_dispatcher.h b/chromium/headless/public/util/url_request_dispatcher.h index 1f34509a3a5..3029dd5c2b8 100644 --- a/chromium/headless/public/util/url_request_dispatcher.h +++ b/chromium/headless/public/util/url_request_dispatcher.h @@ -10,6 +10,7 @@ namespace headless { class ManagedDispatchURLRequestJob; +class NavigationRequest; // Interface to abstract and potentially reorder (for determinism) calls to // ManagedDispatchUrlRequestJob::OnHeadersComplete and @@ -36,6 +37,10 @@ class URLRequestDispatcher { // Tells us the job has finished. Can be called from any thread. virtual void JobDeleted(ManagedDispatchURLRequestJob* job) = 0; + // Tells us a navigation has been requested. Can be called from any thread. + virtual void NavigationRequested( + std::unique_ptr<NavigationRequest> navigation_request) = 0; + private: DISALLOW_COPY_AND_ASSIGN(URLRequestDispatcher); }; diff --git a/chromium/headless/public/version.h.in b/chromium/headless/public/version.h.in new file mode 100644 index 00000000000..7e54cb131cd --- /dev/null +++ b/chromium/headless/public/version.h.in @@ -0,0 +1,13 @@ +// Copyright 2017 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. + +// version.h is generated from version.h.in. Edit the source! + +#ifndef HEADLESS_PUBLIC_VERSION_H_ +#define HEADLESS_PUBLIC_VERSION_H_ + +#define PRODUCT_VERSION "@MAJOR@.@MINOR@.@BUILD@.@PATCH@" +#define LAST_CHANGE "@LASTCHANGE@" + +#endif // HEADLESS_PUBLIC_VERSION_H_ |