blob: 1410313ffa8b663ae36e4f6eb1454a2a551726b1 [file] [log] [blame]
// Copyright 2016 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 <iostream>
#include <memory>
#include <string>
#include "base/base64.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/ref_counted.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_switches.h"
#include "headless/public/domains/page.h"
#include "headless/public/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 "net/base/file_stream.h"
#include "net/base/io_buffer.h"
#include "net/base/ip_address.h"
#include "net/base/net_errors.h"
#include "ui/gfx/geometry/size.h"
using headless::HeadlessBrowser;
using headless::HeadlessBrowserContext;
using headless::HeadlessDevToolsClient;
using headless::HeadlessWebContents;
namespace page = headless::page;
namespace runtime = headless::runtime;
namespace {
// Address where to listen to incoming DevTools connections.
const char kDevToolsHttpServerAddress[] = "127.0.0.1";
// Default file name for screenshot. Can be overriden by "--screenshot" switch.
const char kDefaultScreenshotFileName[] = "screenshot.png";
}
// A sample application which demonstrates the use of the headless API.
class HeadlessShell : public HeadlessWebContents::Observer, page::Observer {
public:
HeadlessShell()
: browser_(nullptr),
devtools_client_(HeadlessDevToolsClient::Create()),
web_contents_(nullptr),
processed_page_ready_(false),
browser_context_(nullptr) {}
~HeadlessShell() override {}
void OnStart(HeadlessBrowser* browser) {
browser_ = browser;
browser_context_ = browser_->CreateBrowserContextBuilder().Build();
HeadlessWebContents::Builder builder(
browser_context_->CreateWebContentsBuilder());
base::CommandLine::StringVector args =
base::CommandLine::ForCurrentProcess()->GetArgs();
if (!args.empty() && !args[0].empty())
builder.SetInitialURL(GURL(args[0]));
web_contents_ = builder.Build();
if (!web_contents_) {
LOG(ERROR) << "Navigation failed";
browser_->Shutdown();
return;
}
web_contents_->AddObserver(this);
}
void Shutdown() {
if (!web_contents_)
return;
if (!RemoteDebuggingEnabled()) {
devtools_client_->GetPage()->RemoveObserver(this);
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 {
if (RemoteDebuggingEnabled())
return;
web_contents_->GetDevToolsTarget()->AttachClient(devtools_client_.get());
devtools_client_->GetPage()->AddObserver(this);
devtools_client_->GetPage()->Enable();
// Check if the document had already finished loading by the time we
// attached.
PollReadyState();
// TODO(skyostil): Implement more features to demonstrate the devtools API.
}
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, base::Unretained(this)));
}
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;
if (ready_state == "complete" &&
(url_.spec() == url || url != "about:blank")) {
OnPageReady();
return;
}
}
}
// page::Observer implementation:
void OnLoadEventFired(const page::LoadEventFiredParams& params) override {
OnPageReady();
}
void OnPageReady() {
if (processed_page_ready_)
return;
processed_page_ready_ = true;
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
headless::switches::kDumpDom)) {
FetchDom();
} else if (base::CommandLine::ForCurrentProcess()->HasSwitch(
headless::switches::kRepl)) {
std::cout
<< "Type a Javascript expression to evaluate or \"quit\" to exit."
<< std::endl;
InputExpression();
} else if (base::CommandLine::ForCurrentProcess()->HasSwitch(
headless::switches::kScreenshot)) {
CaptureScreenshot();
} else {
Shutdown();
}
}
void FetchDom() {
devtools_client_->GetRuntime()->Evaluate(
"document.body.innerHTML",
base::Bind(&HeadlessShell::OnDomFetched, base::Unretained(this)));
}
void OnDomFetched(std::unique_ptr<runtime::EvaluateResult> result) {
if (result->GetWasThrown()) {
LOG(ERROR) << "Failed to evaluate document.body.innerHTML";
} else {
std::string dom;
if (result->GetResult()->GetValue()->GetAsString(&dom)) {
std::cout << dom << std::endl;
}
}
Shutdown();
}
void InputExpression() {
// Note that a real system should read user input asynchronously, because
// otherwise all other browser activity is suspended (e.g., page loading).
std::string expression;
std::cout << ">>> ";
std::getline(std::cin, expression);
if (std::cin.bad() || std::cin.eof() || expression == "quit") {
Shutdown();
return;
}
devtools_client_->GetRuntime()->Evaluate(
expression,
base::Bind(&HeadlessShell::OnExpressionResult, base::Unretained(this)));
}
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);
std::cout << result_json << std::endl;
InputExpression();
}
void CaptureScreenshot() {
devtools_client_->GetPage()->GetExperimental()->CaptureScreenshot(
page::CaptureScreenshotParams::Builder().Build(),
base::Bind(&HeadlessShell::OnScreenshotCaptured,
base::Unretained(this)));
}
void OnScreenshotCaptured(
std::unique_ptr<page::CaptureScreenshotResult> result) {
base::FilePath file_name =
base::CommandLine::ForCurrentProcess()->GetSwitchValuePath(
headless::switches::kScreenshot);
if (file_name.empty()) {
file_name = base::FilePath().AppendASCII(kDefaultScreenshotFileName);
}
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,
base::Unretained(this), 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 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;
}
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,
base::Unretained(this), 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 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 {
std::cout << "Screenshot written to file " << file_name.value() << "."
<< std::endl;
}
int close_result = screenshot_file_stream_->Close(base::Bind(
&HeadlessShell::OnScreenshotFileClosed, base::Unretained(this)));
if (close_result != net::ERR_IO_PENDING) {
// Operation could not be started.
OnScreenshotFileClosed(close_result);
}
}
void OnScreenshotFileClosed(const int close_result) { Shutdown(); }
bool RemoteDebuggingEnabled() const {
const base::CommandLine& command_line =
*base::CommandLine::ForCurrentProcess();
return command_line.HasSwitch(switches::kRemoteDebuggingPort);
}
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_;
DISALLOW_COPY_AND_ASSIGN(HeadlessShell);
};
int main(int argc, const char** argv) {
headless::RunChildProcessIfNeeded(argc, argv);
HeadlessShell shell;
HeadlessBrowser::Options::Builder builder(argc, argv);
// Enable devtools if requested.
base::CommandLine command_line(argc, argv);
if (command_line.HasSwitch(switches::kRemoteDebuggingPort)) {
std::string address = kDevToolsHttpServerAddress;
if (command_line.HasSwitch(headless::switches::kRemoteDebuggingAddress)) {
address = command_line.GetSwitchValueASCII(
headless::switches::kRemoteDebuggingAddress);
net::IPAddress parsed_address;
if (!net::ParseURLHostnameToAddress(address, &parsed_address)) {
LOG(ERROR) << "Invalid devtools server address";
return EXIT_FAILURE;
}
}
int parsed_port;
std::string port_str =
command_line.GetSwitchValueASCII(switches::kRemoteDebuggingPort);
if (!base::StringToInt(port_str, &parsed_port) ||
!base::IsValueInRangeForNumericType<uint16_t>(parsed_port)) {
LOG(ERROR) << "Invalid devtools server port";
return EXIT_FAILURE;
}
net::IPAddress devtools_address;
bool result = devtools_address.AssignFromIPLiteral(address);
DCHECK(result);
builder.EnableDevToolsServer(net::IPEndPoint(
devtools_address, base::checked_cast<uint16_t>(parsed_port)));
}
if (command_line.HasSwitch(headless::switches::kProxyServer)) {
std::string proxy_server =
command_line.GetSwitchValueASCII(headless::switches::kProxyServer);
net::HostPortPair parsed_proxy_server =
net::HostPortPair::FromString(proxy_server);
if (parsed_proxy_server.host().empty() || !parsed_proxy_server.port()) {
LOG(ERROR) << "Malformed proxy server url";
return EXIT_FAILURE;
}
builder.SetProxyServer(parsed_proxy_server);
}
if (command_line.HasSwitch(switches::kHostResolverRules)) {
builder.SetHostResolverRules(
command_line.GetSwitchValueASCII(switches::kHostResolverRules));
}
if (command_line.HasSwitch(headless::switches::kUseGL)) {
builder.SetGLImplementation(
command_line.GetSwitchValueASCII(headless::switches::kUseGL));
}
if (command_line.HasSwitch(headless::switches::kUserDataDir)) {
builder.SetUserDataDir(
command_line.GetSwitchValuePath(headless::switches::kUserDataDir));
builder.SetIncognitoMode(false);
}
return HeadlessBrowserMain(
builder.Build(),
base::Bind(&HeadlessShell::OnStart, base::Unretained(&shell)));
}