blob: 7f7aa7a5cc34b41cae888a8710f473769d7a87c5 [file] [log] [blame]
// Copyright 2016 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/public/headless_shell.h"
#include <memory>
#include "base/base_switches.h"
#include "base/command_line.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/i18n/rtl.h"
#include "base/logging.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/common/content_switches.h"
#include "headless/app/headless_shell_command_line.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_browser.h"
#include "headless/public/headless_browser_context.h"
#include "headless/public/headless_web_contents.h"
#include "headless/public/switches.h"
#include "net/base/filename_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"
#endif
#if defined(HEADLESS_USE_POLICY)
#include "headless/lib/browser/policy/headless_mode_policy.h"
#endif
#if defined(HEADLESS_ENABLE_COMMANDS)
#include "components/headless/command_handler/headless_command_handler.h"
#endif
namespace headless {
namespace {
#if BUILDFLAG(IS_WIN)
const wchar_t kAboutBlank[] = L"about:blank";
#else
const char kAboutBlank[] = "about:blank";
#endif
GURL ConvertArgumentToURL(const base::CommandLine::StringType& arg) {
#if BUILDFLAG(IS_WIN)
GURL url(base::WideToUTF8(arg));
#else
GURL url(arg);
#endif
if (url.is_valid() && url.has_scheme())
return url;
return net::FilePathToFileURL(
base::MakeAbsoluteFilePath(base::FilePath(arg)));
}
// An application which implements a simple headless browser.
class HeadlessShell {
public:
HeadlessShell() = default;
HeadlessShell(const HeadlessShell&) = delete;
HeadlessShell& operator=(const HeadlessShell&) = delete;
~HeadlessShell() = default;
void OnBrowserStart(HeadlessBrowser* browser);
private:
void ShutdownSoon();
void Shutdown();
raw_ptr<HeadlessBrowser> browser_ = nullptr;
};
void HeadlessShell::OnBrowserStart(HeadlessBrowser* browser) {
browser_ = browser;
#if defined(HEADLESS_USE_POLICY)
if (policy::HeadlessModePolicy::IsHeadlessDisabled(
static_cast<HeadlessBrowserImpl*>(browser)->GetPrefs())) {
LOG(ERROR) << "Headless mode is disabled by policy.";
ShutdownSoon();
return;
}
#endif
HeadlessBrowserContext::Builder context_builder =
browser_->CreateBrowserContextBuilder();
// Retrieve the locale set by InitApplicationLocale() in
// 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.
HeadlessBrowserContext* 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 (args.empty() && !IsRemoteDebuggingEnabled())
args.push_back(kAboutBlank);
if (args.empty()) {
return;
}
GURL target_url = ConvertArgumentToURL(args.front());
HeadlessWebContents::Builder builder(
browser_context->CreateWebContentsBuilder());
// If driven by a debugger just open the target page and
// leave expecting the debugger will do what they need.
if (IsRemoteDebuggingEnabled()) {
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* 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, base::Unretained(this)));
#endif
}
void HeadlessShell::ShutdownSoon() {
browser_->BrowserMainThread()->PostTask(
FROM_HERE,
base::BindOnce(&HeadlessShell::Shutdown, base::Unretained(this)));
}
void HeadlessShell::Shutdown() {
browser_->Shutdown();
}
void HeadlessChildMain(content::ContentMainParams params) {
HeadlessContentMainDelegate delegate(nullptr);
params.delegate = &delegate;
int rc = content::ContentMain(std::move(params));
// Note that exiting from here means that base::AtExitManager objects will not
// have a chance to be destroyed (typically in main/WinMain).
// Use TerminateCurrentProcessImmediately instead of exit to avoid shutdown
// crashes and slowdowns on shutdown.
base::Process::TerminateCurrentProcessImmediately(rc);
}
int HeadlessBrowserMain(content::ContentMainParams params) {
base::CommandLine& command_line = *base::CommandLine::ForCurrentProcess();
#if DCHECK_IS_ON()
// The browser can only be initialized once.
static bool browser_was_initialized;
DCHECK(!browser_was_initialized);
browser_was_initialized = true;
// Child processes should not end up here.
DCHECK(!command_line.HasSwitch(::switches::kProcessType));
#endif
HeadlessShell shell;
HeadlessBrowser::Options::Builder builder;
#if defined(HEADLESS_ENABLE_COMMANDS)
if ((command_line.HasSwitch(::switches::kRemoteDebuggingPort) ||
command_line.HasSwitch(::switches::kRemoteDebuggingPipe)) &&
HeadlessCommandHandler::HasHeadlessCommandSwitches(command_line)) {
LOG(ERROR) << "Headless commands are not compatible with remote debugging.";
return EXIT_FAILURE;
}
#endif
if (!HandleCommandLineSwitches(command_line, builder)) {
return EXIT_FAILURE;
}
auto browser = std::make_unique<HeadlessBrowserImpl>(
base::BindOnce(&HeadlessShell::OnBrowserStart, base::Unretained(&shell)),
builder.Build());
HeadlessContentMainDelegate delegate(std::move(browser));
params.delegate = &delegate;
return content::ContentMain(std::move(params));
}
} // namespace
int HeadlessShellMain(content::ContentMainParams params) {
#if BUILDFLAG(IS_WIN)
base::CommandLine::Init(0, nullptr);
#else
base::CommandLine::Init(params.argc, params.argv);
#endif // BUILDFLAG(IS_WIN)
base::CommandLine& command_line(*base::CommandLine::ForCurrentProcess());
std::string process_type =
command_line.GetSwitchValueASCII(::switches::kProcessType);
#if defined(HEADLESS_USE_CRASHPAD)
if (process_type == crash_reporter::switches::kCrashpadHandler) {
return crash_reporter::RunAsCrashpadHandler(
*base::CommandLine::ForCurrentProcess(), base::FilePath(),
::switches::kProcessType, switches::kUserDataDir);
}
#endif // defined(HEADLESS_USE_CRASHPAD)
if (!process_type.empty()) {
HeadlessChildMain(std::move(params));
NOTREACHED();
}
#if BUILDFLAG(IS_MAC)
command_line.AppendSwitch(os_crypt::switches::kUseMockKeychain);
#endif
#if BUILDFLAG(IS_FUCHSIA)
// TODO(fuchsia): Remove this when GPU accelerated compositing is ready.
command_line.AppendSwitch(::switches::kDisableGpu);
#endif
if (command_line.GetArgs().size() > 1) {
LOG(ERROR) << "Multiple targets are not supported.";
return EXIT_FAILURE;
}
return HeadlessBrowserMain(std::move(params));
}
} // namespace headless