0

Reland "[headless] Implemented headless_shell command line switches in JS."

This is a reland of commit 8ed3374708

Improved print-to-pdf tests in order to fix problems
on Fuchsia, see https://luci-milo.appspot.com/ui/inv/build-8794130968083063313/test-results?q=HeadlessPrintToPdfCommandBrowserTest.PrintToPdf&sortby=&groupby=

Added headless command resources pack to Chrome resources to
fix https://crbug.com/1403187 and https://crbug.com/1403340

Original change's description:
> [headless] Implemented headless_shell command line switches in JS.
>
> Headless shell command line switches were implemented in C++ using
> calls to DevTools backend via Simple DevTools Protocol client. This
> is not the most secure way to implement functionality controlld by
> those command line switches, so this CL moves implementation to
> embedded JS.
>
> Bug: 1382571
> Change-Id: If6a76ab8cfbd9319434125c0d8447e483705c72f
> Cq-Include-Trybots: luci.chromium.try:linux-headless-shell-rel
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4061958
> Reviewed-by: Samuel Huang <huangs@chromium.org>
> Commit-Queue: Peter Kvitek <kvitekp@chromium.org>
> Reviewed-by: Andrey Kosyakov <caseq@chromium.org>
> Cr-Commit-Position: refs/heads/main@{#1086091}

Bug: 1382571, 1403187, 1403340
Change-Id: I8a29befbe37aeb0ede8a2663377b6665416f8165
Cq-Include-Trybots: luci.chromium.try:linux-headless-shell-rel
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4128412
Commit-Queue: Peter Kvitek <kvitekp@chromium.org>
Reviewed-by: Andrey Kosyakov <caseq@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1087444}
This commit is contained in:
Peter Kvitek
2022-12-28 23:35:00 +00:00
committed by Chromium LUCI CQ
parent 40ef1e16cc
commit dc9cc51019
28 changed files with 1627 additions and 639 deletions

@ -17,6 +17,9 @@ angle_enable_swiftshader = true
# Embed resource.pak into binary to simplify deployment.
headless_use_embedded_resources = true
# Disable headless commands support.
headless_enable_commands = false
# Don't use Prefs component, disabling access to Local State prefs.
headless_use_prefs = false

@ -9,6 +9,7 @@ import("//build/config/locales.gni")
import("//chrome/browser/buildflags.gni")
import("//chrome/common/features.gni")
import("//extensions/buildflags/buildflags.gni")
import("//headless/headless.gni")
import("//pdf/features.gni")
import("//ui/base/ui_features.gni")
import("chrome_repack_locales.gni")
@ -462,6 +463,11 @@ template("chrome_extra_paks") {
"//chrome/browser/resources/chromeos/chromebox_for_meetings:resources",
]
}
if (headless_enable_commands && !is_android) {
sources += [ "$root_gen_dir/headless/headless_command_resources.pak" ]
deps += [ "//headless:headless_command_resources" ]
}
}
}

@ -70,6 +70,11 @@ void SimpleDevToolsProtocolClient::AttachToWebContents(
AttachClient(DevToolsAgentHost::GetOrCreateFor(web_contents));
}
std::string SimpleDevToolsProtocolClient::GetTargetId() {
DCHECK(agent_host_);
return agent_host_->GetId();
}
std::unique_ptr<SimpleDevToolsProtocolClient>
SimpleDevToolsProtocolClient::CreateSession(const std::string& session_id) {
auto client = std::make_unique<SimpleDevToolsProtocolClient>(session_id);

@ -58,6 +58,8 @@ class SimpleDevToolsProtocolClient : public content::DevToolsAgentHostClient {
void SendCommand(const std::string& method);
std::string GetTargetId();
protected:
// content::DevToolsAgentHostClient implementation.
void DispatchProtocolMessage(content::DevToolsAgentHost* agent_host,

@ -23,10 +23,20 @@ if (headless_use_policy) {
"'headless_use_policy' requires 'headless_use_prefs'.")
}
if (headless_enable_commands) {
assert(
!headless_use_embedded_resources,
"'headless_enable_commands' is not compatible with 'headless_use_embedded_resources'.")
}
# Headless defines config applied to every target below.
config("headless_defines_config") {
defines = []
if (headless_enable_commands) {
defines += [ "HEADLESS_ENABLE_COMMANDS" ]
}
if (headless_use_prefs) {
defines += [ "HEADLESS_USE_PREFS" ]
}
@ -162,6 +172,26 @@ action("embedded_resource_pack_strings") {
deps = [ ":resource_pack_strings" ]
}
if (headless_enable_commands) {
grit("headless_command_resources") {
source = "app/headless_command.grd"
outputs = [
"grit/headless_command_resources.h",
"$root_gen_dir/headless/headless_command_resources.pak",
]
use_brotli = true
}
repack("headless_command_resources_pack") {
sources = [ "$root_gen_dir/headless/headless_command_resources.pak" ]
output = "$root_out_dir/headless_command_resources.pak"
deps = [ ":headless_command_resources" ]
}
}
devtools_domains = [
"accessibility",
"animation",
@ -319,6 +349,13 @@ source_set("headless_shared_sources") {
"public/util/user_agent.h",
]
if (headless_enable_commands) {
sources += [
"app/headless_command_switches.cc",
"app/headless_command_switches.h",
]
}
sources += generated_devtools_api_headers + generated_devtools_api_sources
if (!is_fuchsia) {
@ -779,6 +816,8 @@ test("headless_browsertests") {
"//v8:external_startup_data",
]
sources = [
"test/capture_std_stream.cc",
"test/capture_std_stream.h",
"test/headless_browser_browsertest.cc",
"test/headless_browser_context_browsertest.cc",
"test/headless_browser_test.cc",
@ -797,7 +836,11 @@ test("headless_browsertests") {
]
if (enable_printing && enable_pdf) {
sources += [ "test/headless_printtopdf_browsertest.cc" ]
sources += [
"test/headless_printtopdf_browsertest.cc",
"test/pdf_utils.cc",
"test/pdf_utils.h",
]
}
if (headless_use_policy) {
@ -807,6 +850,10 @@ test("headless_browsertests") {
]
}
if (headless_enable_commands) {
sources += [ "test/headless_command_browsertest.cc" ]
}
# TODO(crbug.com/1318548): Enable on Fuchsia when no longer flakily timeout.
if (!is_fuchsia) {
sources += [
@ -827,6 +874,10 @@ test("headless_browsertests") {
"//third_party/pywebsocket3/",
]
if (headless_enable_commands) {
data += [ "$root_out_dir/headless_command_resources.pak" ]
}
data_deps = []
if (is_fuchsia) {
@ -931,6 +982,17 @@ if (is_win) {
if (enable_printing) {
deps += [ "//components/printing/browser/headless:headless" ]
}
if (headless_enable_commands) {
sources += [
"app/headless_command_handler.cc",
"app/headless_command_handler.h",
]
deps += [
":headless_command_resources",
":headless_command_resources_pack",
]
}
configs += [ ":headless_defines_config" ]
}
}
@ -1000,6 +1062,17 @@ static_library("headless_shell_lib") {
deps += [ "//components/policy/content" ]
}
if (headless_enable_commands) {
sources += [
"app/headless_command_handler.cc",
"app/headless_command_handler.h",
]
deps += [
":headless_command_resources",
":headless_command_resources_pack",
]
}
if (is_win) {
defines = [ "HEADLESS_USE_CRASHPAD" ]

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<grit latest_public_release="0" current_release="1" output_all_resource_defines="false">
<outputs>
<output filename="grit/headless_command_resources.h" type="rc_header">
<emit emit_type='prepend'></emit>
</output>
<output filename="headless_command_resources.pak" type="data_package" />
</outputs>
<translations />
<release seq="1">
<includes>
<include name="IDR_HEADLESS_COMMAND_HTML" file="headless_command.html" type="BINDATA" />
<include name="IDR_HEADLESS_COMMAND_JS" file="headless_command.js" type="BINDATA" />
</includes>
</release>
</grit>

@ -0,0 +1,7 @@
<!-- Copyright 2022 The Chromium Authors
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. -->
<!DOCTYPE html>
<html>
<script src="/headless_command.js"></script>
</html>

@ -0,0 +1,301 @@
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// CDPClient
//
class CDPClient {
constructor() {
this._requestId = 0;
this._sessions = new Map();
}
nextRequestId() {
return ++this._requestId;
}
addSession(session) {
this._sessions.set(session.sessionId(), session);
}
getSession(sessionId) {
this._sessions.get(sessionId);
}
async dispatchMessage(message) {
const messageObject = JSON.parse(message);
const session = this._sessions.get(messageObject.sessionId || '');
if (session)
session.dispatchMessage(messageObject);
}
reportError(message, error) {
if (error)
console.error(`${message}: ${error}\n${error.stack}`);
else
console.error(message);
}
}
const cdpClient = new CDPClient();
//
// CDPSession
//
class CDPSession {
constructor(sessionId) {
this._sessionId = sessionId || '';
this._parentSessionId = null;
this._dispatchTable = new Map();
this._eventHandlers = new Map();
this._protocol = this._getProtocol();
cdpClient.addSession(this);
}
sessionId() {
return this._sessionId;
}
protocol() {
return this._protocol;
}
createSession(sessionId) {
const session = new CDPSession(sessionId);
session._parentSessionId = this._sessionId;
return session;
}
async sendCommand(method, params) {
const requestId = cdpClient.nextRequestId();
const messageObject = {'id': requestId, 'method': method, 'params': params};
if (this._sessionId)
messageObject.sessionId = this._sessionId;
sendDevToolsMessage(JSON.stringify(messageObject));
return new Promise(f => this._dispatchTable.set(requestId, f));
}
async dispatchMessage(message) {
try {
const messageId = message.id;
if (typeof messageId === 'number') {
const handler = this._dispatchTable.get(messageId);
if (handler) {
this._dispatchTable.delete(messageId);
handler(message);
} else {
cdpClient.reportError(`Unexpected result id ${messageId}`);
}
} else {
const eventName = message.method;
for (const handler of (this._eventHandlers.get(eventName) || []))
handler(message);
}
} catch (e) {
cdpClient.reportError(`Exception when dispatching message\n' +
'${JSON.stringify(message)}`, e);
}
}
_getProtocol() {
return new Proxy({}, {
get: (target, domainName, receiver) => new Proxy({}, {
get: (target, methodName, receiver) => {
const eventPattern = /^(on(ce)?|off)([A-Z][A-Za-z0-9]*)/;
const match = eventPattern.exec(methodName);
if (!match) {
return args => this.sendCommand(
`${domainName}.${methodName}`, args || {});
}
let eventName = match[3];
eventName = eventName.charAt(0).toLowerCase() + eventName.slice(1);
if (match[1] === 'once') {
return eventMatcher => this._waitForEvent(
`${domainName}.${eventName}`, eventMatcher);
}
if (match[1] === 'off') {
return listener => this._removeEventHandler(
`${domainName}.${eventName}`, listener);
}
return listener => this._addEventHandler(
`${domainName}.${eventName}`, listener);
}
})
});
}
_waitForEvent(eventName, eventMatcher) {
return new Promise(callback => {
const handler = result => {
if (eventMatcher && !eventMatcher(result))
return;
this._removeEventHandler(eventName, handler);
callback(result);
};
this._addEventHandler(eventName, handler);
});
}
_addEventHandler(eventName, handler) {
const handlers = this._eventHandlers.get(eventName) || [];
handlers.push(handler);
this._eventHandlers.set(eventName, handlers);
}
_removeEventHandler(eventName, handler) {
const handlers = this._eventHandlers.get(eventName) || [];
const index = handlers.indexOf(handler);
if (index === -1)
return;
handlers.splice(index, 1);
this._eventHandlers.set(eventName, handlers);
}
}
//
// TargetPage
//
class TargetPage {
constructor(browserSession) {
this._browserSession = browserSession;
this._targetId = '';
this._session;
}
static async createAndNavigate(browserSession, url) {
const targetPage = new TargetPage(browserSession);
const dp = browserSession.protocol();
const params = {url: 'about:blank', width: 800, height: 600};
const createContextOptions = {};
params.browserContextId = (await dp.Target.createBrowserContext(
createContextOptions)).result.browserContextId;
targetPage._targetId =
(await dp.Target.createTarget(params)).result.targetId;
const sessionId = (await dp.Target.attachToTarget(
{targetId: targetPage._targetId, flatten: true})).result.sessionId;
targetPage._session = browserSession.createSession(sessionId);
await targetPage._navigate(url);
return targetPage;
}
targetId() {
return this._targetId;
}
session() {
return this._session;
}
async _navigate(url) {
const dp = this._session.protocol();
await dp.Page.enable();
await dp.Page.setLifecycleEventsEnabled({enabled: true});
const frameId = (await dp.Page.navigate({url})).result.frameId;
await dp.Page.onceLifecycleEvent(
event => event.params.name === 'load' &&
event.params.frameId === frameId);
}
}
//
// Command handlers
//
async function dumpDOM(dp) {
const script =
"(document.doctype ? new " +
"XMLSerializer().serializeToString(document.doctype) + '\\n' : '')" +
" + document.documentElement.outerHTML";
const response = await dp.Runtime.evaluate({expression: script});
return response.result.result.value;
}
async function printToPDF(dp, params) {
const displayHeaderFooter = !params.noHeaderFooter;
const printToPDFParams = {
displayHeaderFooter,
printBackground: true,
preferCSSPageSize: true,
};
const response = await dp.Page.printToPDF(printToPDFParams);
return response.result.data;
}
async function screenshot(dp, params) {
const format = params.format || 'png';
const screenshotParams = {
format,
};
const response = await dp.Page.captureScreenshot(screenshotParams);
return response.result.data;
}
async function handleCommands(dp, commands) {
const result = {};
if ('dumpDom' in commands)
result.dumpDomResult = await dumpDOM(dp);
if ('printToPDF' in commands)
result.printToPdfResult = await printToPDF(dp, commands.printToPDF);
if ('screenshot' in commands)
result.screenshotResult = await screenshot(dp, commands.screenshot);
return result;
}
//
// Target.exposeDevToolsProtocol() communication functions.
//
window.cdp.onmessage = json => {
//console.log('[recv] ' + json);
cdpClient.dispatchMessage(json);
}
function sendDevToolsMessage(json) {
//console.log('[send] ' + json);
window.cdp.send(json);
}
//
// This is called from the host.
//
async function executeCommands(commands) {
const browserSession = new CDPSession();
const targetPage = await TargetPage.createAndNavigate(
browserSession, commands.targetUrl);
const dp = targetPage.session().protocol();
if ('defaultBackgroundColor' in commands) {
await dp.Emulation.setDefaultBackgroundColorOverride(
{color: commands.defaultBackgroundColor});
}
let promises = [];
if ('timeout' in commands) {
const timeoutPromise = new Promise(resolve => {
setTimeout(resolve, commands.timeout);
});
promises.push(timeoutPromise);
}
if ('virtualTimeBudget' in commands) {
await dp.Emulation.setVirtualTimePolicy(
{budget: commands.virtualTimeBudget,
maxVirtualTimeTaskStarvationCount: 9999,
policy: 'pauseIfNetworkFetchesPending' });
promises.push(dp.Emulation.onceVirtualTimeBudgetExpired());
}
if (promises.length > 0)
await Promise.race(promises);
return await handleCommands(dp, commands);
}

@ -0,0 +1,339 @@
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "headless/app/headless_command_handler.h"
#include <cstdint>
#include <iostream>
#include <map>
#include "base/base64.h"
#include "base/bind.h"
#include "base/callback.h"
#include "base/command_line.h"
#include "base/containers/adapters.h"
#include "base/containers/span.h"
#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/i18n/rtl.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/path_service.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "build/build_config.h"
#include "content/public/app/content_main.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_ui_data_source.h"
#include "headless/app/headless_command_switches.h"
#include "headless/grit/headless_command_resources.h"
#include "ui/base/resource/resource_bundle.h"
namespace headless {
namespace {
// Default file name for screenshot. Can be overridden by "--screenshot" switch.
const char kDefaultScreenshotFileName[] = "screenshot.png";
// Default file name for pdf. Can be overridden by "--print-to-pdf" switch.
const char kDefaultPDFFileName[] = "output.pdf";
const char kChromeHeadlessHost[] = "headless";
const char kChromeHeadlessURL[] = "chrome://headless/";
const char kHeadlessCommandHtml[] = "headless_command.html";
const char kHeadlessCommandJs[] = "headless_command.js";
content::WebUIDataSource* CreateHeadlessHostDataSource() {
base::FilePath resource_dir;
CHECK(base::PathService::Get(base::DIR_ASSETS, &resource_dir));
base::FilePath resource_pack =
resource_dir.Append(FILE_PATH_LITERAL("headless_command_resources.pak"));
CHECK(base::PathExists(resource_pack)) << resource_pack;
ui::ResourceBundle::GetSharedInstance().AddDataPackFromPath(
resource_pack, ui::kScaleFactorNone);
content::WebUIDataSource* source =
content::WebUIDataSource::Create(kChromeHeadlessHost);
source->AddResourcePath(kHeadlessCommandHtml, IDR_HEADLESS_COMMAND_HTML);
source->AddResourcePath(kHeadlessCommandJs, IDR_HEADLESS_COMMAND_JS);
return source;
}
base::Value::Dict GetColorDictFromHexColor(uint32_t color, bool has_alpha) {
base::Value::Dict dict;
if (has_alpha) {
dict.Set("r", static_cast<int>((color & 0xff000000) >> 24));
dict.Set("g", static_cast<int>((color & 0x00ff0000) >> 16));
dict.Set("b", static_cast<int>((color & 0x0000ff00) >> 8));
dict.Set("a", static_cast<int>((color & 0x000000ff)));
} else {
dict.Set("r", static_cast<int>((color & 0xff0000) >> 16));
dict.Set("g", static_cast<int>((color & 0x00ff00) >> 8));
dict.Set("b", static_cast<int>((color & 0x0000ff)));
}
return dict;
}
bool GetCommandDictAndOutputPaths(base::Value::Dict* commands,
base::FilePath* pdf_file_path,
base::FilePath* screenshot_file_path) {
const base::CommandLine* command_line =
base::CommandLine::ForCurrentProcess();
// --dump-dom
if (command_line->HasSwitch(switches::kDumpDom)) {
commands->Set("dumpDom", true);
}
// --print-to-pdf=[output path]
if (command_line->HasSwitch(switches::kPrintToPDF)) {
base::FilePath path =
command_line->GetSwitchValuePath(switches::kPrintToPDF);
if (path.empty()) {
path = base::FilePath().AppendASCII(kDefaultPDFFileName);
}
*pdf_file_path = path;
base::Value::Dict params;
if (command_line->HasSwitch(switches::kPrintToPDFNoHeader)) {
params.Set("noHeaderFooter", true);
}
commands->Set("printToPDF", std::move(params));
}
// --screenshot=[output path]
if (command_line->HasSwitch(switches::kScreenshot)) {
base::FilePath path =
command_line->GetSwitchValuePath(switches::kScreenshot);
if (path.empty()) {
path = base::FilePath().AppendASCII(kDefaultScreenshotFileName);
}
*screenshot_file_path = path;
base::FilePath::StringType extension =
base::ToLowerASCII(path.FinalExtension());
static const std::map<const base::FilePath::StringType, const char*>
kImageFileTypes{
{FILE_PATH_LITERAL(".jpeg"), "jpeg"},
{FILE_PATH_LITERAL(".jpg"), "jpeg"},
{FILE_PATH_LITERAL(".png"), "png"},
{FILE_PATH_LITERAL(".webp"), "webp"},
};
auto it = kImageFileTypes.find(extension);
if (it == kImageFileTypes.cend()) {
LOG(ERROR) << "Unsupported screenshot image file type: "
<< path.FinalExtension();
return false;
}
base::Value::Dict params;
params.Set("format", it->second);
commands->Set("screenshot", std::move(params));
}
// --default-background-color=rrggbb[aa]
if (command_line->HasSwitch(switches::kDefaultBackgroundColor)) {
std::string hex_color =
command_line->GetSwitchValueASCII(switches::kDefaultBackgroundColor);
uint32_t color;
if (!(hex_color.length() == 6 || hex_color.length() == 8) ||
!base::HexStringToUInt(hex_color, &color)) {
LOG(ERROR)
<< "Expected a hex RGB or RGBA value for --default-background-color="
<< hex_color;
return false;
}
commands->Set("defaultBackgroundColor",
GetColorDictFromHexColor(color, hex_color.length() == 8));
}
// virtual-time-budget=[ms]
if (command_line->HasSwitch(switches::kVirtualTimeBudget)) {
std::string budget_ms_str =
command_line->GetSwitchValueASCII(switches::kVirtualTimeBudget);
int budget_ms;
if (!base::StringToInt(budget_ms_str, &budget_ms)) {
LOG(ERROR) << "Expected an integer value for --virtual-time-budget="
<< budget_ms_str;
return false;
}
commands->Set("virtualTimeBudget", budget_ms);
}
// timeout=[ms]
if (command_line->HasSwitch(switches::kTimeout)) {
std::string timeout_ms_str =
command_line->GetSwitchValueASCII(switches::kTimeout);
int timeout_ms;
if (!base::StringToInt(timeout_ms_str, &timeout_ms)) {
LOG(ERROR) << "Expected an integer value for --timeout="
<< timeout_ms_str;
return false;
}
commands->Set("timeout", timeout_ms);
}
return true;
}
void WriteFileTask(base::FilePath file_path, std::string file_data) {
auto file_span = base::make_span(
reinterpret_cast<const uint8_t*>(file_data.data()), file_data.size());
if (base::WriteFile(file_path, file_span)) {
std::cerr << file_data.size() << " bytes written to file " << file_path
<< std::endl;
} else {
PLOG(ERROR) << "Failed to write file " << file_path;
}
}
void WriteFile(base::FilePath file_path, std::string base64_file_data) {
std::string file_data;
CHECK(base::Base64Decode(base64_file_data, &file_data));
base::ThreadPool::CreateSequencedTaskRunner(
{base::MayBlock(), base::TaskPriority::USER_BLOCKING,
base::TaskShutdownBehavior::BLOCK_SHUTDOWN})
->PostTask(FROM_HERE, base::BindOnce(&WriteFileTask, std::move(file_path),
std::move(file_data)));
}
} // namespace
HeadlessCommandHandler::HeadlessCommandHandler(
content::WebContents* web_contents,
GURL target_url,
DoneCallback done_callback)
: web_contents_(web_contents),
target_url_(std::move(target_url)),
done_callback_(std::move(done_callback)) {
// Load command execution harness resources and create URL data source
// for chrome://headless.
content::WebUIDataSource::Add(web_contents_->GetBrowserContext(),
CreateHeadlessHostDataSource());
content::WebContentsObserver::Observe(web_contents_);
browser_devtools_client_.AttachToBrowser();
devtools_client_.AttachToWebContents(web_contents_);
}
HeadlessCommandHandler::~HeadlessCommandHandler() = default;
// static
GURL HeadlessCommandHandler::GetHandlerUrl() {
const std::string url =
base::StrCat({kChromeHeadlessURL, kHeadlessCommandHtml});
return GURL(url);
}
// static
void HeadlessCommandHandler::ProcessCommands(content::WebContents* web_contents,
GURL target_url,
DoneCallback done_callback) {
// Headless Command Handler instance will self delete when done.
HeadlessCommandHandler* command_handler = new HeadlessCommandHandler(
web_contents, std::move(target_url), std::move(done_callback));
command_handler->ExecuteCommands();
}
void HeadlessCommandHandler::ExecuteCommands() {
// Expose DevTools protocol to the target.
base::Value::Dict params;
params.Set("targetId", devtools_client_.GetTargetId());
browser_devtools_client_.SendCommand("Target.exposeDevToolsProtocol",
std::move(params));
// Set up Inspector domain.
devtools_client_.AddEventHandler(
"Inspector.targetCrashed",
base::BindRepeating(&HeadlessCommandHandler::OnTargetCrashed,
base::Unretained(this)));
devtools_client_.SendCommand("Inspector.enable");
}
void HeadlessCommandHandler::DocumentOnLoadCompletedInPrimaryMainFrame() {
base::Value::Dict commands;
if (!GetCommandDictAndOutputPaths(&commands, &pdf_file_path_,
&screenshot_file_path_) ||
commands.empty()) {
Done();
return;
}
commands.Set("targetUrl", target_url_.spec());
std::string json_commands;
base::JSONWriter::Write(commands, &json_commands);
std::string script = "executeCommands(JSON.parse('" + json_commands + "'))";
base::Value::Dict params;
params.Set("expression", script);
params.Set("awaitPromise", true);
params.Set("returnByValue", true);
devtools_client_.SendCommand(
"Runtime.evaluate", std::move(params),
base::BindOnce(&HeadlessCommandHandler::OnCommandsResult,
base::Unretained(this)));
}
void HeadlessCommandHandler::WebContentsDestroyed() {
CHECK(false);
}
void HeadlessCommandHandler::OnTargetCrashed(const base::Value::Dict&) {
LOG(ERROR) << "Abnormal renderer termination.";
Done();
}
void HeadlessCommandHandler::OnCommandsResult(base::Value::Dict result) {
if (std::string* dom_dump =
result.FindStringByDottedPath("result.result.value.dumpDomResult")) {
std::cout << *dom_dump << std::endl;
}
if (std::string* base64_data = result.FindStringByDottedPath(
"result.result.value.screenshotResult")) {
WriteFile(std::move(screenshot_file_path_), std::move(*base64_data));
}
if (std::string* base64_data = result.FindStringByDottedPath(
"result.result.value.printToPdfResult")) {
WriteFile(std::move(pdf_file_path_), std::move(*base64_data));
}
Done();
}
void HeadlessCommandHandler::Done() {
DCHECK(web_contents_);
devtools_client_.DetachClient();
browser_devtools_client_.DetachClient();
DoneCallback done_callback(std::move(done_callback_));
delete this;
std::move(done_callback).Run();
}
} // namespace headless

@ -0,0 +1,68 @@
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef HEADLESS_APP_HEADLESS_COMMAND_HANDLER_H_
#define HEADLESS_APP_HEADLESS_COMMAND_HANDLER_H_
#include "base/files/file_path.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/values.h"
#include "components/devtools/simple_devtools_protocol_client/simple_devtools_protocol_client.h"
#include "content/public/browser/web_contents_observer.h"
#include "url/gurl.h"
namespace content {
class WebContents;
} // namespace content
namespace headless {
class HeadlessCommandHandler : public content::WebContentsObserver {
public:
typedef base::OnceCallback<void()> DoneCallback;
HeadlessCommandHandler(const HeadlessCommandHandler&) = delete;
HeadlessCommandHandler& operator=(const HeadlessCommandHandler&) = delete;
static GURL GetHandlerUrl();
static void ProcessCommands(content::WebContents* web_contents,
GURL target_url,
DoneCallback done_callback);
private:
using SimpleDevToolsProtocolClient =
simple_devtools_protocol_client::SimpleDevToolsProtocolClient;
HeadlessCommandHandler(content::WebContents* web_contents,
GURL target_url,
DoneCallback done_callback);
~HeadlessCommandHandler() override;
void ExecuteCommands();
// content::WebContentsObserver implementation:
void DocumentOnLoadCompletedInPrimaryMainFrame() override;
void WebContentsDestroyed() override;
void OnTargetCrashed(const base::Value::Dict&);
void OnCommandsResult(base::Value::Dict result);
void Done();
SimpleDevToolsProtocolClient devtools_client_;
SimpleDevToolsProtocolClient browser_devtools_client_;
raw_ptr<content::WebContents> web_contents_;
GURL target_url_;
DoneCallback done_callback_;
base::FilePath pdf_file_path_;
base::FilePath screenshot_file_path_;
};
} // namespace headless
#endif // HEADLESS_APP_HEADLESS_COMMAND_HANDLER_H_

@ -0,0 +1,38 @@
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "headless/app/headless_command_switches.h"
namespace headless::switches {
// The background color to be used if the page doesn't specify one. Provided as
// RGB or RGBA integer value in hex, e.g. 'ff0000ff' for red or '00000000' for
// transparent.
const char kDefaultBackgroundColor[] = "default-background-color";
// Instructs headless_shell to print document.body.innerHTML to stdout.
const char kDumpDom[] = "dump-dom";
// Save a pdf file of the loaded page.
const char kPrintToPDF[] = "print-to-pdf";
// Do not display header and footer in the pdf file.
const char kPrintToPDFNoHeader[] = "print-to-pdf-no-header";
// Save a screenshot of the loaded page.
const char kScreenshot[] = "screenshot";
// Issues a stop after the specified number of milliseconds. This cancels all
// navigation and causes the DOMContentLoaded event to fire.
const char kTimeout[] = "timeout";
// If set the system waits the specified number of virtual milliseconds before
// deeming the page to be ready. For determinism virtual time does not advance
// while there are pending network fetches (i.e no timers will fire). Once all
// network fetches have completed, timers fire and if the system runs out of
// virtual time is fastforwarded so the next timer fires immediately, until the
// specified virtual time budget is exhausted.
const char kVirtualTimeBudget[] = "virtual-time-budget";
} // namespace headless::switches

@ -0,0 +1,22 @@
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef HEADLESS_APP_HEADLESS_COMMAND_SWITCHES_H_
#define HEADLESS_APP_HEADLESS_COMMAND_SWITCHES_H_
#include "headless/public/headless_export.h"
namespace headless::switches {
HEADLESS_EXPORT extern const char kDefaultBackgroundColor[];
HEADLESS_EXPORT extern const char kDumpDom[];
HEADLESS_EXPORT extern const char kPrintToPDF[];
HEADLESS_EXPORT extern const char kPrintToPDFNoHeader[];
HEADLESS_EXPORT extern const char kScreenshot[];
HEADLESS_EXPORT extern const char kTimeout[];
HEADLESS_EXPORT extern const char kVirtualTimeBudget[];
} // namespace headless::switches
#endif // HEADLESS_APP_HEADLESS_COMMAND_SWITCHES_H_

@ -2,49 +2,38 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <cstdint>
#include <memory>
#include <sstream>
#include <utility>
#include "headless/app/headless_shell.h"
#include <memory>
#include "base/base64.h"
#include "base/base_switches.h"
#include "base/bind.h"
#include "base/callback.h"
#include "base/command_line.h"
#include "base/containers/adapters.h"
#include "base/containers/span.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/i18n/rtl.h"
#include "base/json/json_writer.h"
#include "base/location.h"
#include "base/numerics/safe_conversions.h"
#include "base/path_service.h"
#include "base/process/process.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/thread_pool.h"
#include "build/branding_buildflags.h"
#include "build/build_config.h"
#include "content/public/app/content_main.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "headless/app/headless_shell.h"
#include "headless/app/headless_command_handler.h"
#include "headless/app/headless_shell_command_line.h"
#include "headless/app/headless_shell_switches.h"
#include "headless/lib/browser/headless_browser_impl.h"
#include "headless/lib/browser/headless_web_contents_impl.h"
#include "headless/lib/headless_content_main_delegate.h"
#include "headless/public/headless_devtools_target.h"
#include "headless/public/headless_browser.h"
#include "headless/public/headless_browser_context.h"
#include "headless/public/headless_web_contents.h"
#include "net/base/filename_util.h"
#include "net/http/http_util.h"
#include "url/gurl.h"
#if BUILDFLAG(IS_MAC)
#include "components/os_crypt/os_crypt_switches.h" // nogncheck
#endif
#if BUILDFLAG(IS_WIN)
#include "base/strings/utf_string_conversions.h"
#include "components/crash/core/app/crash_switches.h" // nogncheck
#include "components/crash/core/app/run_as_crashpad_handler_win.h"
#include "sandbox/win/src/sandbox_types.h"
@ -54,6 +43,10 @@
#include "headless/lib/browser/policy/headless_mode_policy.h"
#endif
#if defined(HEADLESS_ENABLE_COMMANDS)
#include "headless/app/headless_command_handler.h"
#endif
namespace headless {
namespace {
@ -64,11 +57,6 @@ const wchar_t kAboutBlank[] = L"about:blank";
const char kAboutBlank[] = "about:blank";
#endif
// Default file name for screenshot. Can be overridden by "--screenshot" switch.
const char kDefaultScreenshotFileName[] = "screenshot.png";
// Default file name for pdf. Can be overridden by "--print-to-pdf" switch.
const char kDefaultPDFFileName[] = "output.pdf";
GURL ConvertArgumentToURL(const base::CommandLine::StringType& arg) {
#if BUILDFLAG(IS_WIN)
GURL url(base::WideToUTF8(arg));
@ -82,32 +70,6 @@ GURL ConvertArgumentToURL(const base::CommandLine::StringType& arg) {
base::MakeAbsoluteFilePath(base::FilePath(arg)));
}
base::Value::Dict GetColorDictFromHexColor(const std::string& color_hex) {
uint32_t color;
CHECK(base::HexStringToUInt(color_hex, &color))
<< "Expected a hex value for --default-background-color=";
base::Value::Dict dict;
dict.Set("r", static_cast<int>((color & 0xff000000) >> 24));
dict.Set("g", static_cast<int>((color & 0x00ff0000) >> 16));
dict.Set("b", static_cast<int>((color & 0x0000ff00) >> 8));
dict.Set("a", static_cast<int>((color & 0x000000ff)));
return dict;
}
bool DoWriteFile(const base::FilePath& file_path, std::string file_data) {
auto file_span = base::make_span(
reinterpret_cast<const uint8_t*>(file_data.data()), file_data.size());
bool success = base::WriteFile(file_path, file_span);
PLOG_IF(ERROR, !success) << "Failed to write file " << file_path;
if (!success)
return false;
LOG(INFO) << file_data.size() << " bytes written to file " << file_path;
return true;
}
} // namespace
HeadlessShell::HeadlessShell() = default;
@ -125,9 +87,6 @@ void HeadlessShell::OnBrowserStart(HeadlessBrowser* browser) {
}
#endif
file_task_runner_ = base::ThreadPool::CreateSequencedTaskRunner(
{base::MayBlock(), base::TaskPriority::BEST_EFFORT});
HeadlessBrowserContext::Builder context_builder =
browser_->CreateBrowserContextBuilder();
@ -135,387 +94,70 @@ void HeadlessShell::OnBrowserStart(HeadlessBrowser* browser) {
// headless_content_main_delegate.cc in a way that is free of side-effects.
context_builder.SetAcceptLanguage(base::i18n::GetConfiguredLocale());
// Create browser 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.
browser_context_ = context_builder.Build();
browser_->SetDefaultBrowserContext(browser_context_);
// If no explicit URL is present navigate to about:blank unless we're being
// driven by a debugger.
base::CommandLine::StringVector args =
base::CommandLine::ForCurrentProcess()->GetArgs();
// If no explicit URL is present, navigate to about:blank, unless we're being
// driven by a debugger.
if (args.empty() && !IsRemoteDebuggingEnabled())
args.push_back(kAboutBlank);
if (!args.empty()) {
file_task_runner_->PostTaskAndReplyWithResult(
FROM_HERE, base::BindOnce(&ConvertArgumentToURL, args.front()),
base::BindOnce(&HeadlessShell::OnCommandLineURL,
weak_factory_.GetWeakPtr()));
}
}
void HeadlessShell::OnCommandLineURL(const GURL& url) {
HeadlessWebContents::Builder builder(
browser_context_->CreateWebContentsBuilder());
HeadlessWebContents* web_contents = builder.SetInitialURL(url).Build();
if (!web_contents) {
LOG(ERROR) << "Navigation to " << url << " failed";
browser_->Shutdown();
if (args.empty()) {
return;
}
// Unless we're in remote debugging mode, associate target and
// start observing it so we can run commands.
if (!IsRemoteDebuggingEnabled()) {
url_ = url;
web_contents_ = web_contents;
web_contents_->AddObserver(this);
}
}
GURL target_url = ConvertArgumentToURL(args.front());
void HeadlessShell::Detach() {
if (web_contents_) {
devtools_client_.DetachClient();
web_contents_->RemoveObserver(this);
web_contents_ = nullptr;
// If driven by a debugger just open the target page and
// leave expecting the debugger will do what they need.
if (IsRemoteDebuggingEnabled()) {
HeadlessWebContents::Builder builder(
browser_context_->CreateWebContentsBuilder());
HeadlessWebContents* web_contents =
builder.SetInitialURL(target_url).Build();
if (!web_contents) {
LOG(ERROR) << "Navigation to " << target_url << " failed.";
ShutdownSoon();
}
return;
}
// Otherwise instantiate headless shell command handler that will
// execute the commands against the target page.
#if defined(HEADLESS_ENABLE_COMMANDS)
GURL handler_url = HeadlessCommandHandler::GetHandlerUrl();
HeadlessWebContents::Builder builder(
browser_context_->CreateWebContentsBuilder());
HeadlessWebContents* web_contents =
builder.SetInitialURL(handler_url).Build();
if (!web_contents) {
LOG(ERROR) << "Navigation to " << handler_url << " failed.";
ShutdownSoon();
return;
}
HeadlessCommandHandler::ProcessCommands(
HeadlessWebContentsImpl::From(web_contents)->web_contents(),
std::move(target_url),
base::BindOnce(&HeadlessShell::ShutdownSoon, weak_factory_.GetWeakPtr()));
#endif
}
void HeadlessShell::ShutdownSoon() {
if (shutdown_pending_)
return;
shutdown_pending_ = true;
DCHECK(browser_);
browser_->BrowserMainThread()->PostTask(
FROM_HERE,
base::BindOnce(&HeadlessShell::Shutdown, weak_factory_.GetWeakPtr()));
}
void HeadlessShell::Shutdown() {
if (web_contents_)
web_contents_->Close();
DCHECK(!web_contents_);
browser_->Shutdown();
}
void HeadlessShell::DevToolsTargetReady() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
devtools_client_.AttachToWebContents(
HeadlessWebContentsImpl::From(web_contents_)->web_contents());
HeadlessDevToolsTarget* target = web_contents_->GetDevToolsTarget();
if (!target->IsAttached()) {
LOG(ERROR) << "Could not attach DevTools target.";
ShutdownSoon();
return;
}
devtools_client_.AddEventHandler(
"Inspector.targetCrashed",
base::BindRepeating(&HeadlessShell::OnTargetCrashed,
weak_factory_.GetWeakPtr()));
devtools_client_.AddEventHandler(
"Page.loadEventFired",
base::BindRepeating(&HeadlessShell::OnLoadEventFired,
weak_factory_.GetWeakPtr()));
devtools_client_.SendCommand("Page.enable");
devtools_client_.AddEventHandler(
"Emulation.virtualTimeBudgetExpired",
base::BindRepeating(&HeadlessShell::OnVirtualTimeBudgetExpired,
weak_factory_.GetWeakPtr()));
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kDefaultBackgroundColor)) {
std::string color_hex =
base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
switches::kDefaultBackgroundColor);
base::Value::Dict params;
params.Set("color", GetColorDictFromHexColor(color_hex));
devtools_client_.SendCommand("Emulation.setDefaultBackgroundColorOverride",
std::move(params));
}
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=";
base::Value::Dict params;
params.Set("budget", budget_ms);
params.Set("policy", "pauseIfNetworkFetchesPending");
devtools_client_.SendCommand("Emulation.setVirtualTimePolicy",
std::move(params));
} else {
// Check if the document had already finished loading by the time we
// attached.
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::BindOnce(&HeadlessShell::FetchTimeout,
weak_factory_.GetWeakPtr()),
base::Milliseconds(timeout_ms));
}
}
void HeadlessShell::HeadlessWebContentsDestroyed() {
// Detach now, but defer shutdown till the HeadlessWebContents
// removal is complete.
Detach();
ShutdownSoon();
}
void HeadlessShell::FetchTimeout() {
LOG(INFO) << "Timeout.";
devtools_client_.SendCommand("Page.stopLoading");
// After calling page.stopLoading() the page will not fire any
// life cycle events, so we have to proceed on our own.
browser_->BrowserMainThread()->PostTask(
FROM_HERE,
base::BindOnce(&HeadlessShell::OnPageReady, weak_factory_.GetWeakPtr()));
}
void HeadlessShell::OnTargetCrashed(const base::Value::Dict&) {
LOG(ERROR) << "Abnormal renderer termination.";
// NB this never gets called if remote debugging is enabled.
ShutdownSoon();
}
void HeadlessShell::PollReadyState() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// We need to check the current location in addition to the ready state to
// be sure the expected page is ready.
base::Value::Dict params;
params.Set("expression",
"document.readyState + ' ' + document.location.href");
devtools_client_.SendCommand(
"Runtime.evaluate", std::move(params),
base::BindOnce(&HeadlessShell::OnEvaluateReadyStateResult,
weak_factory_.GetWeakPtr()));
}
void HeadlessShell::OnEvaluateReadyStateResult(base::Value::Dict result) {
const std::string* result_value =
result.FindStringByDottedPath("result.result.value");
if (!result_value)
return;
std::stringstream stream(*result_value);
std::string ready_state;
std::string url;
stream >> ready_state;
stream >> url;
if (ready_state == "complete" &&
(url_.spec() == url || url != "about:blank")) {
OnPageReady();
return;
}
}
void HeadlessShell::OnVirtualTimeBudgetExpired(const base::Value::Dict&) {
OnPageReady();
}
void HeadlessShell::OnLoadEventFired(const base::Value::Dict&) {
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kVirtualTimeBudget)) {
return;
}
OnPageReady();
}
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 if (base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kPrintToPDF)) {
PrintToPDF();
} else {
ShutdownSoon();
}
}
void HeadlessShell::FetchDom() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
base::Value::Dict params;
params.Set(
"expression",
"(document.doctype ? new "
"XMLSerializer().serializeToString(document.doctype) + '\\n' : '') + "
"document.documentElement.outerHTML");
devtools_client_.SendCommand(
"Runtime.evaluate", std::move(params),
base::BindOnce(&HeadlessShell::OnEvaluateFetchDomResult,
weak_factory_.GetWeakPtr()));
}
void HeadlessShell::OnEvaluateFetchDomResult(base::Value::Dict result) {
if (const base::Value::Dict* result_exception_details =
result.FindDictByDottedPath("result.exceptionDetails")) {
LOG(ERROR) << "Failed to serialize document:\n"
<< *result_exception_details->FindStringByDottedPath(
"exception.description");
} else if (const std::string* result_value =
result.FindStringByDottedPath("result.result.value")) {
printf("%s\n", result_value->c_str());
}
ShutdownSoon();
}
void HeadlessShell::InputExpression() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// 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 == '\n')
break;
if (c == EOF) {
// If there's no expression, then quit.
if (expression.str().size() == 0) {
printf("\n");
ShutdownSoon();
return;
}
break;
}
expression << static_cast<char>(c);
}
if (expression.str() == "quit") {
ShutdownSoon();
return;
}
base::Value::Dict params;
params.Set("expression", expression.str());
devtools_client_.SendCommand(
"Runtime.evaluate", std::move(params),
base::BindOnce(&HeadlessShell::OnEvaluateExpressionResult,
weak_factory_.GetWeakPtr()));
}
void HeadlessShell::OnEvaluateExpressionResult(base::Value::Dict result) {
std::string result_json;
base::JSONWriter::Write(result, &result_json);
printf("%s\n", result_json.c_str());
InputExpression();
}
void HeadlessShell::CaptureScreenshot() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
devtools_client_.SendCommand(
"Page.captureScreenshot",
base::BindOnce(&HeadlessShell::OnCaptureScreenshotResult,
weak_factory_.GetWeakPtr()));
}
void HeadlessShell::OnCaptureScreenshotResult(base::Value::Dict result) {
const std::string* result_data = result.FindStringByDottedPath("result.data");
if (!result_data) {
LOG(ERROR) << "Capture screenshot failed";
ShutdownSoon();
return;
}
std::string data;
if (!base::Base64Decode(*result_data, &data)) {
LOG(ERROR) << "Invalid screenshot data";
return;
}
WriteFile(switches::kScreenshot, kDefaultScreenshotFileName, std::move(data));
}
void HeadlessShell::PrintToPDF() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
base::Value::Dict params;
params.Set("printBackground", true);
params.Set("preferCSSPageSize", true);
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kPrintToPDFNoHeader)) {
params.Set("displayHeaderFooter", false);
}
devtools_client_.SendCommand("Page.printToPDF", std::move(params),
base::BindOnce(&HeadlessShell::OnPrintToPDFDone,
weak_factory_.GetWeakPtr()));
}
void HeadlessShell::OnPrintToPDFDone(base::Value::Dict result) {
const std::string* result_data = result.FindStringByDottedPath("result.data");
if (!result_data) {
LOG(ERROR) << "Print to PDF failed";
ShutdownSoon();
return;
}
std::string data;
if (!base::Base64Decode(*result_data, &data)) {
LOG(ERROR) << "Invalid PDF data";
return;
}
WriteFile(switches::kPrintToPDF, kDefaultPDFFileName, std::move(data));
}
void HeadlessShell::WriteFile(const std::string& file_path_switch,
const std::string& default_file_name,
std::string data) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
base::FilePath file_name =
base::CommandLine::ForCurrentProcess()->GetSwitchValuePath(
file_path_switch);
if (file_name.empty())
file_name = base::FilePath().AppendASCII(default_file_name);
file_task_runner_->PostTaskAndReplyWithResult(
FROM_HERE, base::BindOnce(&DoWriteFile, file_name, std::move(data)),
base::BindOnce(&HeadlessShell::OnWriteFileDone,
weak_factory_.GetWeakPtr()));
}
void HeadlessShell::OnWriteFileDone(bool success) {
ShutdownSoon();
}
#if BUILDFLAG(IS_WIN)
int HeadlessShellMain(HINSTANCE instance,
sandbox::SandboxInterfaceInfo* sandbox_info) {

@ -5,84 +5,32 @@
#ifndef HEADLESS_APP_HEADLESS_SHELL_H_
#define HEADLESS_APP_HEADLESS_SHELL_H_
#include <string>
#include <vector>
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/task/sequenced_task_runner.h"
#include "base/values.h"
#include "components/devtools/simple_devtools_protocol_client/simple_devtools_protocol_client.h"
#include "headless/public/headless_browser.h"
#include "headless/public/headless_web_contents.h"
#include "url/gurl.h"
class GURL;
namespace headless {
class HeadlessBrowser;
class HeadlessBrowserContext;
// An application which implements a simple headless browser.
class HeadlessShell : public HeadlessWebContents::Observer {
class HeadlessShell {
public:
HeadlessShell();
HeadlessShell(const HeadlessShell&) = delete;
HeadlessShell& operator=(const HeadlessShell&) = delete;
~HeadlessShell() override;
~HeadlessShell();
void OnBrowserStart(HeadlessBrowser* browser);
private:
// HeadlessWebContents::Observer implementation:
void DevToolsTargetReady() override;
void HeadlessWebContentsDestroyed() override;
void OnTargetCrashed(const base::Value::Dict&);
void OnLoadEventFired(const base::Value::Dict&);
void OnVirtualTimeBudgetExpired(const base::Value::Dict&);
void Detach();
void ShutdownSoon();
void Shutdown();
void FetchTimeout();
void OnCommandLineURL(const GURL& url);
void PollReadyState();
void OnEvaluateReadyStateResult(base::Value::Dict result);
void OnPageReady();
void FetchDom();
void OnEvaluateFetchDomResult(base::Value::Dict result);
void InputExpression();
void OnEvaluateExpressionResult(base::Value::Dict result);
void CaptureScreenshot();
void OnCaptureScreenshotResult(base::Value::Dict result);
void PrintToPDF();
void OnPrintToPDFDone(base::Value::Dict result);
void WriteFile(const std::string& file_path_switch,
const std::string& default_file_name,
std::string data);
void OnWriteFileDone(bool success);
GURL url_;
raw_ptr<HeadlessBrowser> browser_ = nullptr; // Not owned.
simple_devtools_protocol_client::SimpleDevToolsProtocolClient
devtools_client_;
raw_ptr<HeadlessWebContents> web_contents_ = nullptr;
raw_ptr<HeadlessBrowserContext> browser_context_ = nullptr;
scoped_refptr<base::SequencedTaskRunner> file_task_runner_;
bool processed_page_ready_ = false;
bool shutdown_pending_ = false;
base::WeakPtrFactory<HeadlessShell> weak_factory_{this};
};

@ -6,8 +6,8 @@
#include <cstdio>
#include "base/bind.h"
#include "base/logging.h"
#include "build/build_config.h"
#include "cc/base/switches.h"
#include "components/viz/common/switches.h"
#include "content/public/common/content_switches.h"
@ -20,13 +20,19 @@
#include "ui/gfx/font_render_params.h"
#include "ui/gfx/geometry/size.h"
#if defined(HEADLESS_ENABLE_COMMANDS)
#include "headless/app/headless_command_switches.h"
#endif
namespace headless {
namespace {
// By default listen to incoming DevTools connections on localhost.
const char kLocalHost[] = "localhost";
bool ValidateCommandLineSwitches(const base::CommandLine& command_line) {
#if defined(HEADLESS_ENABLE_COMMANDS)
if (command_line.HasSwitch(switches::kRemoteDebuggingPort) ||
command_line.HasSwitch(switches::kRemoteDebuggingPipe)) {
static const char* kIncompatibleSwitches[] = {
@ -47,6 +53,7 @@ bool ValidateCommandLineSwitches(const base::CommandLine& command_line) {
}
}
}
#endif // defined(HEADLESS_ENABLE_COMMANDS)
return true;
}

@ -7,11 +7,6 @@
namespace headless {
namespace switches {
// The background color to be used if the page doesn't specify one. Provided as
// RGBA integer value in hex, e.g. 'ff0000ff' for red or '00000000' for
// transparent.
const char kDefaultBackgroundColor[] = "default-background-color";
// Whether cookies stored as part of user profile are encrypted.
const char kDisableCookieEncryption[] = "disable-cookie-encryption";
@ -38,9 +33,6 @@ const char kDeterministicMode[] = "deterministic-mode";
// UserDatadir.
const char kDiskCacheDir[] = "disk-cache-dir";
// Instructs headless_shell to print document.body.innerHTML to stdout.
const char kDumpDom[] = "dump-dom";
// Specifies which encryption storage backend to use. Possible values are
// kwallet, kwallet5, gnome, gnome-keyring, gnome-libsecret, basic. Any other
// value will lead to Chrome detecting the best backend automatically.
@ -51,12 +43,6 @@ const char kDumpDom[] = "dump-dom";
// or KWallets.
const char kPasswordStore[] = "password-store";
// Save a pdf file of the loaded page.
const char kPrintToPDF[] = "print-to-pdf";
// Do not display header and footer in the pdf file.
const char kPrintToPDFNoHeader[] = "print-to-pdf-no-header";
// Do not emit tags when printing PDFs.
const char kDisablePDFTagging[] = "disable-pdf-tagging";
@ -83,13 +69,6 @@ const char kRemoteDebuggingAddress[] = "remote-debugging-address";
// expressions.
const char kRepl[] = "repl";
// Save a screenshot of the loaded page.
const char kScreenshot[] = "screenshot";
// Issues a stop after the specified number of milliseconds. This cancels all
// navigation and causes the DOMContentLoaded event to fire.
const char kTimeout[] = "timeout";
// Sets the GL implementation to use. Use a blank string to disable GL
// rendering.
const char kUseGL[] = "use-gl";
@ -110,14 +89,6 @@ const char kUserDataDir[] = "user-data-dir";
// --user-data-dir switch.
const char kIncognito[] = "incognito";
// If set the system waits the specified number of virtual milliseconds before
// deeming the page to be ready. For determinism virtual time does not advance
// while there are pending network fetches (i.e no timers will fire). Once all
// network fetches have completed, timers fire and if the system runs out of
// virtual time is fastforwarded so the next timer fires immediatley, until the
// specified virtual time budget is exhausted.
const char kVirtualTimeBudget[] = "virtual-time-budget";
// Sets the initial window size. Provided as string in the format "800,600".
const char kWindowSize[] = "window-size";

@ -12,31 +12,24 @@ namespace headless {
namespace switches {
HEADLESS_EXPORT extern const char kCrashDumpsDir[];
HEADLESS_EXPORT extern const char kDefaultBackgroundColor[];
HEADLESS_EXPORT extern const char kDeterministicMode[];
HEADLESS_EXPORT extern const char kDisableCookieEncryption[];
HEADLESS_EXPORT extern const char kDisableCrashReporter[];
HEADLESS_EXPORT extern const char kDiskCacheDir[];
HEADLESS_EXPORT extern const char kDumpDom[];
HEADLESS_EXPORT extern const char kEnableBeginFrameControl[];
HEADLESS_EXPORT extern const char kEnableCrashReporter[];
HEADLESS_EXPORT extern const char kPasswordStore[];
HEADLESS_EXPORT extern const char kPrintToPDF[];
HEADLESS_EXPORT extern const char kPrintToPDFNoHeader[];
HEADLESS_EXPORT extern const char kDisablePDFTagging[];
HEADLESS_EXPORT extern const char kProxyBypassList[];
HEADLESS_EXPORT extern const char kProxyServer[];
HEADLESS_EXPORT extern const char kNoSystemProxyConfigService[];
HEADLESS_EXPORT extern const char kRemoteDebuggingAddress[];
HEADLESS_EXPORT extern const char kRepl[];
HEADLESS_EXPORT extern const char kScreenshot[];
HEADLESS_EXPORT extern const char kTimeout[];
HEADLESS_EXPORT extern const char kUseANGLE[];
HEADLESS_EXPORT extern const char kUseGL[];
HEADLESS_EXPORT extern const char kUserAgent[];
HEADLESS_EXPORT extern const char kUserDataDir[];
HEADLESS_EXPORT extern const char kIncognito[];
HEADLESS_EXPORT extern const char kVirtualTimeBudget[];
HEADLESS_EXPORT extern const char kWindowSize[];
HEADLESS_EXPORT extern const char kAuthServerAllowlist[];
HEADLESS_EXPORT extern const char kFontRenderHinting[];

@ -6,6 +6,10 @@ declare_args() {
# Embed resource.pak file into the binary for easier distribution.
headless_use_embedded_resources = false
# Enable support for --screenshot, --print-to-pdf and --dump-dom commands
# Note: this option is not available if |headless_use_embedded_resources|.
headless_enable_commands = true
# Use Prefs component to access Local State and other preferences.
headless_use_prefs = true

@ -0,0 +1,88 @@
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "headless/test/capture_std_stream.h"
#include <fcntl.h>
#include <stdio.h>
#include "base/check_op.h"
#include "build/build_config.h"
#if BUILDFLAG(IS_WIN)
#include <io.h>
#else
#include <unistd.h>
#endif
namespace headless {
namespace {
enum { kReadPipe, kWritePipe };
static constexpr char kPipeEnd = '\xff';
} // namespace
CaptureStdStream::CaptureStdStream(FILE* stream) : stream_(stream) {
#if BUILDFLAG(IS_WIN)
CHECK_EQ(_pipe(pipes_, 4096, O_BINARY), 0);
#else
CHECK_EQ(pipe(pipes_), 0);
#endif
fileno_ = dup(fileno(stream_));
CHECK_NE(fileno_, -1);
}
CaptureStdStream::~CaptureStdStream() {
StopCapture();
close(pipes_[kReadPipe]);
close(pipes_[kWritePipe]);
close(fileno_);
}
void CaptureStdStream::StartCapture() {
if (capturing_) {
return;
}
fflush(stream_);
CHECK_NE(dup2(pipes_[kWritePipe], fileno(stream_)), -1);
capturing_ = true;
}
void CaptureStdStream::StopCapture() {
if (!capturing_) {
return;
}
char eop = kPipeEnd;
CHECK_NE(write(pipes_[kWritePipe], &eop, sizeof(eop)), -1);
fflush(stream_);
CHECK_NE(dup2(fileno_, fileno(stream_)), -1);
capturing_ = false;
}
std::string CaptureStdStream::TakeCapturedData() {
CHECK(!capturing_);
std::string captured_data;
for (;;) {
constexpr size_t kChunkSize = 256;
char buffer[kChunkSize];
int bytes_read = read(pipes_[kReadPipe], buffer, kChunkSize);
CHECK_GT(bytes_read, 0);
if (buffer[bytes_read - 1] != kPipeEnd) {
captured_data.append(buffer, bytes_read);
} else {
captured_data.append(buffer, bytes_read - 1);
break;
}
}
return captured_data;
}
} // namespace headless

@ -0,0 +1,48 @@
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef HEADLESS_TEST_CAPTURE_STD_STREAM_H_
#define HEADLESS_TEST_CAPTURE_STD_STREAM_H_
#include <cstdio>
#include <string>
#include "base/threading/thread_restrictions.h"
namespace headless {
// A class to capture data sent to a standard stream.
class CaptureStdStream {
public:
explicit CaptureStdStream(FILE* stream);
~CaptureStdStream();
void StartCapture();
void StopCapture();
std::string TakeCapturedData();
private:
FILE* stream_;
int fileno_ = -1;
int pipes_[2] = {-1, -1};
bool capturing_ = false;
base::ScopedAllowBlockingForTesting allow_blocking_calls_;
};
class CaptureStdOut : public CaptureStdStream {
public:
CaptureStdOut() : CaptureStdStream(stdout) {}
};
class CaptureStdErr : public CaptureStdStream {
public:
CaptureStdErr() : CaptureStdStream(stderr) {}
};
} // namespace headless
#endif // HEADLESS_TEST_CAPTURE_STD_STREAM_H_

@ -0,0 +1,14 @@
<!doctype html>
<style>
body {
overflow: hidden;
min-width: 300px;
min-height: 300px;
background-image: linear-gradient(#0000ff, #0000ff);
background-size: 100px auto;
background-position: center;
background-repeat: no-repeat;
}
</style>
<body>
</body>

@ -0,0 +1,13 @@
<html>
<body>
<div id="box"></div>
<script>
let timer_count = 0;
function callback() {
document.getElementById('box').textContent = timer_count++;
window.setTimeout(callback, 1000);
}
callback();
</script>
</body>
</html>

@ -0,0 +1,333 @@
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <cmath>
#include <string>
#include "base/bind.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/threading/thread_restrictions.h"
#include "build/build_config.h"
#include "content/public/test/browser_test.h"
#include "headless/app/headless_command_handler.h"
#include "headless/app/headless_command_switches.h"
#include "headless/lib/browser/headless_browser_context_impl.h"
#include "headless/lib/browser/headless_browser_impl.h"
#include "headless/lib/browser/headless_web_contents_impl.h"
#include "headless/public/headless_browser.h"
#include "headless/public/headless_browser_context.h"
#include "headless/public/headless_web_contents.h"
#include "headless/test/capture_std_stream.h"
#include "headless/test/headless_browser_test.h"
#include "headless/test/headless_browser_test_utils.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "pdf/buildflags.h"
#include "printing/buildflags/buildflags.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/gfx/codec/png_codec.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/rect.h"
#include "url/gurl.h"
#if BUILDFLAG(ENABLE_PRINTING) && BUILDFLAG(ENABLE_PDF)
#include "headless/test/pdf_utils.h"
#endif
namespace headless {
namespace {
bool DecodePNG(const std::string& png_data, SkBitmap* bitmap) {
return gfx::PNGCodec::Decode(
reinterpret_cast<const unsigned char*>(png_data.data()), png_data.size(),
bitmap);
}
} // namespace
class HeadlessCommandBrowserTest : public HeadlessBrowserTest {
public:
HeadlessCommandBrowserTest() = default;
void RunTest() {
ASSERT_TRUE(embedded_test_server()->Start());
HeadlessBrowserContext::Builder context_builder =
browser()->CreateBrowserContextBuilder();
HeadlessBrowserContext* browser_context = context_builder.Build();
browser()->SetDefaultBrowserContext(browser_context);
GURL handler_url = HeadlessCommandHandler::GetHandlerUrl();
HeadlessWebContents::Builder builder(
browser_context->CreateWebContentsBuilder());
HeadlessWebContents* web_contents =
builder.SetInitialURL(handler_url).Build();
HeadlessCommandHandler::ProcessCommands(
HeadlessWebContentsImpl::From(web_contents)->web_contents(),
GetTargetUrl(),
base::BindOnce(&HeadlessCommandBrowserTest::FinishTest,
base::Unretained(this)));
RunAsynchronousTest();
web_contents->Close();
browser_context->Close();
base::RunLoop().RunUntilIdle();
}
private:
virtual GURL GetTargetUrl() = 0;
void FinishTest() { FinishAsynchronousTest(); }
};
class HeadlessDumpDomCommandBrowserTest : public HeadlessCommandBrowserTest {
public:
HeadlessDumpDomCommandBrowserTest() = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
HeadlessCommandBrowserTest::SetUpCommandLine(command_line);
command_line->AppendSwitch(switches::kDumpDom);
}
GURL GetTargetUrl() override {
return embedded_test_server()->GetURL("/hello.html");
}
};
IN_PROC_BROWSER_TEST_F(HeadlessDumpDomCommandBrowserTest, DumpDom) {
base::ScopedAllowBlockingForTesting allow_blocking;
CaptureStdOut capture_stdout;
capture_stdout.StartCapture();
RunTest();
capture_stdout.StopCapture();
std::string captured_stdout = capture_stdout.TakeCapturedData();
static const char kDomDump[] =
"<!DOCTYPE html>\n"
"<html><head></head><body><h1>Hello headless world!</h1>\n"
"</body></html>\n";
EXPECT_THAT(captured_stdout, testing::HasSubstr(kDomDump));
}
class HeadlessDumpDomVirtualTimeBudgetCommandBrowserTest
: public HeadlessDumpDomCommandBrowserTest {
public:
HeadlessDumpDomVirtualTimeBudgetCommandBrowserTest() = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
HeadlessDumpDomCommandBrowserTest::SetUpCommandLine(command_line);
command_line->AppendSwitchASCII(switches::kVirtualTimeBudget, "5500");
}
GURL GetTargetUrl() override {
return embedded_test_server()->GetURL("/stepper_page.html");
}
};
IN_PROC_BROWSER_TEST_F(HeadlessDumpDomVirtualTimeBudgetCommandBrowserTest,
DumpDomVirtualTimeBudget) {
base::ScopedAllowBlockingForTesting allow_blocking;
CaptureStdOut capture_stdout;
capture_stdout.StartCapture();
RunTest();
capture_stdout.StopCapture();
std::vector<std::string> captured_lines =
base::SplitString(capture_stdout.TakeCapturedData(), "\n",
base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
EXPECT_THAT(captured_lines, testing::Contains(R"(<div id="box">5</div>)"));
}
class HeadlessFileCommandBrowserTest : public HeadlessCommandBrowserTest {
public:
HeadlessFileCommandBrowserTest() = default;
void SetUp() override {
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
ASSERT_TRUE(base::IsDirectoryEmpty(temp_dir()));
HeadlessCommandBrowserTest::SetUp();
}
void TearDown() override {
HeadlessCommandBrowserTest::TearDown();
ASSERT_TRUE(temp_dir_.Delete());
}
const base::FilePath& temp_dir() const { return temp_dir_.GetPath(); }
base::ScopedTempDir temp_dir_;
};
class HeadlessScreenshotCommandBrowserTest
: public HeadlessFileCommandBrowserTest {
public:
HeadlessScreenshotCommandBrowserTest() = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
HeadlessFileCommandBrowserTest::SetUpCommandLine(command_line);
screenshot_filename_ =
temp_dir().Append(FILE_PATH_LITERAL("screenshot.png"));
command_line->AppendSwitchPath(switches::kScreenshot, screenshot_filename_);
}
GURL GetTargetUrl() override {
return embedded_test_server()->GetURL("/centered_blue_box.html");
}
base::FilePath screenshot_filename_;
};
IN_PROC_BROWSER_TEST_F(HeadlessScreenshotCommandBrowserTest, Screenshot) {
base::ScopedAllowBlockingForTesting allow_blocking;
RunTest();
ASSERT_TRUE(base::PathExists(screenshot_filename_)) << screenshot_filename_;
std::string png_data;
ASSERT_TRUE(base::ReadFileToString(screenshot_filename_, &png_data))
<< screenshot_filename_;
SkBitmap bitmap;
ASSERT_TRUE(DecodePNG(png_data, &bitmap));
ASSERT_EQ(800, bitmap.width());
ASSERT_EQ(600, bitmap.height());
// Expect white background and a centered blue rectangle.
EXPECT_EQ(SkColorSetRGB(0xff, 0xff, 0xff), bitmap.getColor(1, 1));
EXPECT_EQ(SkColorSetRGB(0xff, 0xff, 0xff), bitmap.getColor(800 - 1, 600 - 1));
EXPECT_EQ(SkColorSetRGB(0x00, 0x00, 0xff), bitmap.getColor(800 / 2, 1));
}
class HeadlessScreenshotWithBackgroundCommandBrowserTest
: public HeadlessScreenshotCommandBrowserTest {
public:
HeadlessScreenshotWithBackgroundCommandBrowserTest() = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
HeadlessScreenshotCommandBrowserTest::SetUpCommandLine(command_line);
command_line->AppendSwitchASCII(switches::kDefaultBackgroundColor,
"ff0000");
}
};
IN_PROC_BROWSER_TEST_F(HeadlessScreenshotWithBackgroundCommandBrowserTest,
ScreenshotBackground) {
base::ScopedAllowBlockingForTesting allow_blocking;
RunTest();
ASSERT_TRUE(base::PathExists(screenshot_filename_)) << screenshot_filename_;
std::string png_data;
ASSERT_TRUE(base::ReadFileToString(screenshot_filename_, &png_data))
<< screenshot_filename_;
SkBitmap bitmap;
ASSERT_TRUE(DecodePNG(png_data, &bitmap));
ASSERT_EQ(800, bitmap.width());
ASSERT_EQ(600, bitmap.height());
// Expect red background and a centered blue rectangle.
EXPECT_EQ(SkColorSetRGB(0xff, 0x00, 0x00), bitmap.getColor(1, 1));
EXPECT_EQ(SkColorSetRGB(0xff, 0x00, 0x00), bitmap.getColor(800 - 1, 600 - 1));
EXPECT_EQ(SkColorSetRGB(0x00, 0x00, 0xff), bitmap.getColor(800 / 2, 1));
}
#if BUILDFLAG(ENABLE_PRINTING) && BUILDFLAG(ENABLE_PDF)
class HeadlessPrintToPdfCommandBrowserTest
: public HeadlessFileCommandBrowserTest {
public:
static constexpr float kPageMarginsInInches =
0.393701; // See Page.PrintToPDF specs.
HeadlessPrintToPdfCommandBrowserTest() = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
HeadlessFileCommandBrowserTest::SetUpCommandLine(command_line);
print_to_pdf_filename_ =
temp_dir().Append(FILE_PATH_LITERAL("print_to.pdf"));
command_line->AppendSwitchPath(switches::kPrintToPDF,
print_to_pdf_filename_);
command_line->AppendSwitch(switches::kPrintToPDFNoHeader);
}
GURL GetTargetUrl() override {
return embedded_test_server()->GetURL("/centered_blue_box.html");
}
base::FilePath print_to_pdf_filename_;
};
IN_PROC_BROWSER_TEST_F(HeadlessPrintToPdfCommandBrowserTest, PrintToPdf) {
base::ScopedAllowBlockingForTesting allow_blocking;
RunTest();
ASSERT_TRUE(base::PathExists(print_to_pdf_filename_))
<< print_to_pdf_filename_;
std::string pdf_data;
ASSERT_TRUE(base::ReadFileToString(print_to_pdf_filename_, &pdf_data))
<< print_to_pdf_filename_;
PDFPageBitmap page_bitmap;
ASSERT_TRUE(page_bitmap.Render(pdf_data, 0));
// Expect blue rectangle on white background.
EXPECT_TRUE(page_bitmap.CheckRect(0x0000ff, 0xffffff));
}
class HeadlessPrintToPdfWithBackgroundCommandBrowserTest
: public HeadlessPrintToPdfCommandBrowserTest {
public:
HeadlessPrintToPdfWithBackgroundCommandBrowserTest() = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
HeadlessPrintToPdfCommandBrowserTest::SetUpCommandLine(command_line);
command_line->AppendSwitchASCII(switches::kDefaultBackgroundColor,
"ff0000");
}
};
IN_PROC_BROWSER_TEST_F(HeadlessPrintToPdfWithBackgroundCommandBrowserTest,
PrintToPdfBackground) {
base::ScopedAllowBlockingForTesting allow_blocking;
RunTest();
ASSERT_TRUE(base::PathExists(print_to_pdf_filename_))
<< print_to_pdf_filename_;
std::string pdf_data;
ASSERT_TRUE(base::ReadFileToString(print_to_pdf_filename_, &pdf_data))
<< print_to_pdf_filename_;
PDFPageBitmap page_bitmap;
ASSERT_TRUE(page_bitmap.Render(pdf_data, 0));
// Expect blue rectangle on red background sans margin.
EXPECT_TRUE(page_bitmap.CheckRect(0x0000ff, 0xff0000, 120));
}
#endif // BUILDFLAG(ENABLE_PRINTING) && BUILDFLAG(ENABLE_PDF)
} // namespace headless

@ -23,6 +23,7 @@
#include "content/public/test/browser_test.h"
#include "headless/lib/browser/policy/headless_mode_policy.h"
#include "headless/public/headless_browser.h"
#include "headless/test/capture_std_stream.h"
#include "headless/test/headless_browser_test.h"
#include "headless/test/headless_browser_test_utils.h"
#include "net/base/host_port_pair.h"
@ -116,85 +117,6 @@ IN_PROC_BROWSER_TEST_F(HeadlessBrowserTestWithUrlBlockPolicy, BlockUrl) {
EXPECT_EQ(error, net::ERR_BLOCKED_BY_ADMINISTRATOR);
}
namespace {
class CaptureStdErr {
public:
CaptureStdErr() {
#if BUILDFLAG(IS_WIN)
CHECK_EQ(_pipe(pipes_, 4096, O_BINARY), 0);
#else
CHECK_EQ(pipe(pipes_), 0);
#endif
stderr_ = dup(fileno(stderr));
CHECK_NE(stderr_, -1);
}
~CaptureStdErr() {
StopCapture();
close(pipes_[kReadPipe]);
close(pipes_[kWritePipe]);
close(stderr_);
}
void StartCapture() {
if (capturing_)
return;
fflush(stderr);
CHECK_NE(dup2(pipes_[kWritePipe], fileno(stderr)), -1);
capturing_ = true;
}
void StopCapture() {
if (!capturing_)
return;
char eop = kPipeEnd;
CHECK_NE(write(pipes_[kWritePipe], &eop, sizeof(eop)), -1);
fflush(stderr);
CHECK_NE(dup2(stderr_, fileno(stderr)), -1);
capturing_ = false;
}
std::string ReadCapturedData() {
CHECK(!capturing_);
std::string captured_data;
for (;;) {
constexpr size_t kChunkSize = 256;
char buffer[kChunkSize];
int bytes_read = read(pipes_[kReadPipe], buffer, kChunkSize);
CHECK_NE(bytes_read, -1);
captured_data.append(buffer, bytes_read);
if (captured_data.rfind(kPipeEnd) != std::string::npos)
break;
}
return captured_data;
}
std::vector<std::string> ReadCapturedLines() {
return base::SplitString(ReadCapturedData(), "\n", base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY);
}
private:
enum { kReadPipe, kWritePipe };
static constexpr char kPipeEnd = '\xff';
base::ScopedAllowBlockingForTesting allow_blocking_calls_;
bool capturing_ = false;
int pipes_[2] = {-1, -1};
int stderr_ = -1;
};
} // namespace
class HeadlessBrowserTestWithRemoteDebuggingAllowedPolicy
: public HeadlessBrowserTestWithPolicy<HeadlessBrowserTest>,
public testing::WithParamInterface<bool> {
@ -239,8 +161,12 @@ IN_PROC_BROWSER_TEST_P(HeadlessBrowserTestWithRemoteDebuggingAllowedPolicy,
base::PlatformThread::Sleep(TestTimeouts::action_timeout());
capture_stderr_.StopCapture();
std::vector<std::string> captured_lines =
base::SplitString(capture_stderr_.TakeCapturedData(), "\n",
base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
enum { kUnknown, kDisallowed, kListening } remote_debugging_state = kUnknown;
for (const std::string& line : capture_stderr_.ReadCapturedLines()) {
for (const std::string& line : captured_lines) {
LOG(INFO) << "stderr: " << line;
if (base::MatchPattern(line, "DevTools remote debugging is disallowed *")) {
EXPECT_EQ(remote_debugging_state, kUnknown);

@ -20,6 +20,7 @@
#include "headless/test/headless_browser_test.h"
#include "headless/test/headless_browser_test_utils.h"
#include "headless/test/headless_devtooled_browsertest.h"
#include "headless/test/pdf_utils.h"
#include "pdf/pdf.h"
#include "printing/buildflags/buildflags.h"
#include "printing/pdf_render_settings.h"
@ -34,59 +35,6 @@
namespace headless {
namespace {
// Utility class to render the specified PDF page into a bitmap and
// inspect the resulting pixels.
class PDFPageBitmap {
public:
static constexpr int kColorChannels = 4;
static constexpr int kDpi = 300;
PDFPageBitmap() = default;
~PDFPageBitmap() = default;
void Render(base::span<const uint8_t> pdf_span, int page_index) {
absl::optional<gfx::SizeF> page_size_in_points =
chrome_pdf::GetPDFPageSizeByIndex(pdf_span, page_index);
ASSERT_TRUE(page_size_in_points.has_value());
gfx::SizeF page_size_in_pixels =
gfx::ScaleSize(page_size_in_points.value(),
static_cast<float>(kDpi) / printing::kPointsPerInch);
gfx::Rect page_rect(gfx::ToCeiledSize(page_size_in_pixels));
constexpr chrome_pdf::RenderOptions options = {
.stretch_to_bounds = false,
.keep_aspect_ratio = true,
.autorotate = true,
.use_color = true,
.render_device_type = chrome_pdf::RenderDeviceType::kPrinter,
};
bitmap_size_ = page_rect.size();
bitmap_data_.resize(kColorChannels * bitmap_size_.GetArea());
ASSERT_TRUE(chrome_pdf::RenderPDFPageToBitmap(
pdf_span, page_index, bitmap_data_.data(), bitmap_size_,
gfx::Size(kDpi, kDpi), options));
}
uint32_t GetPixelRGB(int x, int y) {
int pixel_index =
bitmap_size_.width() * y * kColorChannels + x * kColorChannels;
return bitmap_data_[pixel_index + 0] // B
| (bitmap_data_[pixel_index + 1] << 8) // G
| (bitmap_data_[pixel_index + 2] << 16); // R
}
protected:
std::vector<uint8_t> bitmap_data_;
gfx::Size bitmap_size_;
};
} // namespace
class HeadlessPDFPagesBrowserTest : public HeadlessDevTooledBrowserTest {
public:
const double kPaperWidth = 10;
@ -417,7 +365,7 @@ class HeadlessPDFOOPIFBrowserTest : public HeadlessPDFBrowserTestBase {
EXPECT_THAT(num_pages, testing::Eq(1));
PDFPageBitmap page_image;
page_image.Render(pdf_span, 0);
ASSERT_TRUE(page_image.Render(pdf_span, 0));
// Expect red iframe pixel at 1 inch into the page.
EXPECT_EQ(page_image.GetPixelRGB(1 * PDFPageBitmap::kDpi,

120
headless/test/pdf_utils.cc Normal file

@ -0,0 +1,120 @@
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "headless/test/pdf_utils.h"
#include "base/logging.h"
#include "pdf/pdf.h"
#include "printing/units.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size_conversions.h"
#include "ui/gfx/geometry/size_f.h"
namespace headless {
PDFPageBitmap::PDFPageBitmap() = default;
PDFPageBitmap::~PDFPageBitmap() = default;
bool PDFPageBitmap::Render(const std::string& pdf_data, int page_index) {
auto pdf_span = base::make_span(
reinterpret_cast<const uint8_t*>(pdf_data.data()), pdf_data.size());
return Render(pdf_span, page_index);
}
bool PDFPageBitmap::Render(base::span<const uint8_t> pdf_data, int page_index) {
absl::optional<gfx::SizeF> page_size_in_points =
chrome_pdf::GetPDFPageSizeByIndex(pdf_data, page_index);
if (!page_size_in_points) {
return false;
}
gfx::SizeF page_size_in_pixels =
gfx::ScaleSize(page_size_in_points.value(),
static_cast<float>(kDpi) / printing::kPointsPerInch);
gfx::Rect page_rect(gfx::ToCeiledSize(page_size_in_pixels));
constexpr chrome_pdf::RenderOptions options = {
.stretch_to_bounds = false,
.keep_aspect_ratio = true,
.autorotate = true,
.use_color = true,
.render_device_type = chrome_pdf::RenderDeviceType::kPrinter,
};
bitmap_size_ = page_rect.size();
bitmap_data_.resize(kColorChannels * bitmap_size_.GetArea());
return chrome_pdf::RenderPDFPageToBitmap(pdf_data, page_index,
bitmap_data_.data(), bitmap_size_,
gfx::Size(kDpi, kDpi), options);
}
uint32_t PDFPageBitmap::GetPixelRGB(const gfx::Point& pt) const {
return GetPixelRGB(pt.x(), pt.y());
}
uint32_t PDFPageBitmap::GetPixelRGB(int x, int y) const {
CHECK_LT(x, bitmap_size_.width());
CHECK_LT(y, bitmap_size_.height());
int pixel_index =
bitmap_size_.width() * y * kColorChannels + x * kColorChannels;
return bitmap_data_[pixel_index + 0] // B
| (bitmap_data_[pixel_index + 1] << 8) // G
| (bitmap_data_[pixel_index + 2] << 16); // R
}
bool PDFPageBitmap::CheckRect(uint32_t rect_color,
uint32_t bkgr_color,
int margins) {
gfx::Rect body(bitmap_size_);
if (margins) {
body.Inset(margins);
}
// Build color rectangle by including every pixel with the specified
// rectangle color into a rectangle.
gfx::Rect rect;
for (int y = body.y(); y < body.bottom(); y++) {
for (int x = body.x(); x < body.right(); x++) {
uint32_t color = GetPixelRGB(x, y);
if (color == rect_color) {
gfx::Rect pixel_rect(x, y, 1, 1);
if (rect.IsEmpty()) {
rect = pixel_rect;
} else {
rect.Union(pixel_rect);
}
}
}
}
// Verify that all pixels outside the found color rectangle are of
// the specified background color, and the ones that are inside
// the found rectangle are all of the rectangle color.
for (int y = body.y(); y < body.bottom(); y++) {
for (int x = body.x(); x < body.right(); x++) {
gfx::Point pt(x, y);
uint32_t color = GetPixelRGB(pt);
if (rect.Contains(pt)) {
if (color != rect_color) {
LOG(ERROR) << "pt=" << pt.ToString() << " color=" << color
<< ", expected rect color=" << rect_color;
return false;
}
} else {
if (color != bkgr_color) {
LOG(ERROR) << "pt=" << pt.ToString() << " color=" << color
<< ", expected bkgr color=" << bkgr_color;
return false;
}
}
}
}
return true;
}
} // namespace headless

49
headless/test/pdf_utils.h Normal file

@ -0,0 +1,49 @@
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef HEADLESS_TEST_PDF_UTILS_H_
#define HEADLESS_TEST_PDF_UTILS_H_
#include <cstdint>
#include <string>
#include <vector>
#include "base/containers/span.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/size.h"
namespace headless {
// Utility class to render PDF page into a bitmap and inspect its pixels.
class PDFPageBitmap {
public:
static constexpr int kDpi = 300;
static constexpr int kColorChannels = 4;
PDFPageBitmap();
~PDFPageBitmap();
bool Render(const std::string& pdf_data, int page_index);
bool Render(base::span<const uint8_t> pdf_data, int page_index);
uint32_t GetPixelRGB(const gfx::Point& pt) const;
uint32_t GetPixelRGB(int x, int y) const;
bool CheckRect(uint32_t rect_color, uint32_t bkgr_color, int margins);
bool CheckRect(uint32_t rect_color, uint32_t bkgr_color) {
return CheckRect(rect_color, bkgr_color, /*margins=*/0);
}
int width() const { return bitmap_size_.width(); }
int height() const { return bitmap_size_.height(); }
gfx::Size size() const { return bitmap_size_; }
private:
std::vector<uint8_t> bitmap_data_;
gfx::Size bitmap_size_;
};
} // namespace headless
#endif // HEADLESS_TEST_PDF_UTILS_H_

@ -963,6 +963,10 @@
"messages": [4380],
},
"headless/app/headless_command.grd": {
"includes": [4410],
},
"mojo/public/js/mojo_bindings_resources.grd": {
"includes": [4420],
},