| // 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 |