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:

committed by
Chromium LUCI CQ

parent
40ef1e16cc
commit
dc9cc51019
build/args
chrome
components/devtools/simple_devtools_protocol_client
headless
BUILD.gn
app
headless_command.grdheadless_command.htmlheadless_command.jsheadless_command_handler.ccheadless_command_handler.hheadless_command_switches.ccheadless_command_switches.hheadless_shell.ccheadless_shell.hheadless_shell_command_line.ccheadless_shell_switches.ccheadless_shell_switches.h
headless.gnitest
tools/gritsettings
@ -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" ]
|
||||
|
||||
|
16
headless/app/headless_command.grd
Normal file
16
headless/app/headless_command.grd
Normal file
@ -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>
|
7
headless/app/headless_command.html
Normal file
7
headless/app/headless_command.html
Normal file
@ -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>
|
301
headless/app/headless_command.js
Normal file
301
headless/app/headless_command.js
Normal file
@ -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);
|
||||
}
|
339
headless/app/headless_command_handler.cc
Normal file
339
headless/app/headless_command_handler.cc
Normal file
@ -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
|
68
headless/app/headless_command_handler.h
Normal file
68
headless/app/headless_command_handler.h
Normal file
@ -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_
|
38
headless/app/headless_command_switches.cc
Normal file
38
headless/app/headless_command_switches.cc
Normal file
@ -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
|
22
headless/app/headless_command_switches.h
Normal file
22
headless/app/headless_command_switches.h
Normal file
@ -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
|
||||
|
||||
|
88
headless/test/capture_std_stream.cc
Normal file
88
headless/test/capture_std_stream.cc
Normal file
@ -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
|
48
headless/test/capture_std_stream.h
Normal file
48
headless/test/capture_std_stream.h
Normal file
@ -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_
|
14
headless/test/data/centered_blue_box.html
Normal file
14
headless/test/data/centered_blue_box.html
Normal file
@ -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>
|
13
headless/test/data/stepper_page.html
Normal file
13
headless/test/data/stepper_page.html
Normal file
@ -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>
|
333
headless/test/headless_command_browsertest.cc
Normal file
333
headless/test/headless_command_browsertest.cc
Normal file
@ -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
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
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],
|
||||
},
|
||||
|
Reference in New Issue
Block a user