mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-10-19 15:43:20 +00:00

We currently create a separate headless-browser application to serve two purposes: 1. Allow headless browsing to take a screenshot of a page or print its layout tree / internal text. 2. Run the LibWeb test framework. This patch migrates (1) to the main Ladybird executable. The --headless flag enables this mode. This matches the behavior of other browsers, and means we have one less executable to ship at distribution time. We want to avoid creating too many AppKit / Qt facilities in headless mode. So this involves some shuffling of application init to ensure we don't create them until after we've parsed the command line arguments. Namely, we avoid creating the NSApp in AppKit and QCoreApplication in Qt. Doing so also requires that we don't create the application event loop until we've parsed the command line as well, because the loop we create depends on whether we're creating those UI facilities.
139 lines
5.3 KiB
C++
139 lines
5.3 KiB
C++
/*
|
|
* Copyright (c) 2022-2025, Tim Flynn <trflynn89@laybird.org>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <AK/Platform.h>
|
|
#include <LibCore/ArgsParser.h>
|
|
#include <LibCore/Directory.h>
|
|
#include <LibCore/EventLoop.h>
|
|
#include <LibCore/Process.h>
|
|
#include <LibCore/StandardPaths.h>
|
|
#include <LibCore/System.h>
|
|
#include <LibCore/TCPServer.h>
|
|
#include <LibMain/Main.h>
|
|
#include <LibWeb/WebDriver/Capabilities.h>
|
|
#include <LibWebView/Utilities.h>
|
|
#include <WebDriver/Client.h>
|
|
|
|
static Vector<ByteString> certificates;
|
|
|
|
static ErrorOr<Core::Process> launch_process(StringView application, ReadonlySpan<ByteString> arguments)
|
|
{
|
|
auto paths = TRY(WebView::get_paths_for_helper_process(application));
|
|
|
|
ErrorOr<Core::Process> result = Error::from_string_literal("All paths failed to launch");
|
|
for (auto const& path : paths) {
|
|
auto path_view = path.view();
|
|
result = Core::Process::spawn(path_view, arguments, {}, Core::Process::KeepAsChild::Yes);
|
|
if (!result.is_error())
|
|
break;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static Vector<ByteString> create_arguments(ByteString const& socket_path, bool headless, bool force_cpu_painting, Optional<StringView> debug_process)
|
|
{
|
|
Vector<ByteString> arguments {
|
|
"--webdriver-content-path"sv,
|
|
socket_path,
|
|
};
|
|
|
|
Vector<ByteString> certificate_args;
|
|
for (auto const& certificate : certificates) {
|
|
certificate_args.append(ByteString::formatted("--certificate={}", certificate));
|
|
arguments.append(certificate_args.last().view().characters_without_null_termination());
|
|
}
|
|
|
|
if (headless)
|
|
arguments.append("--headless"sv);
|
|
|
|
arguments.append("--allow-popups"sv);
|
|
arguments.append("--force-new-process"sv);
|
|
arguments.append("--enable-autoplay"sv);
|
|
arguments.append("--disable-scrollbar-painting"sv);
|
|
if (force_cpu_painting)
|
|
arguments.append("--force-cpu-painting"sv);
|
|
|
|
if (debug_process.has_value())
|
|
arguments.append(ByteString::formatted("--debug-process={}", debug_process.value()));
|
|
|
|
// FIXME: WebDriver does not yet handle the WebContent process switch brought by site isolation.
|
|
arguments.append("--disable-site-isolation"sv);
|
|
|
|
arguments.append("about:blank"sv);
|
|
return arguments;
|
|
}
|
|
|
|
ErrorOr<int> serenity_main(Main::Arguments arguments)
|
|
{
|
|
AK::set_rich_debug_enabled(true);
|
|
|
|
auto listen_address = "0.0.0.0"sv;
|
|
int port = 8000;
|
|
bool force_cpu_painting = false;
|
|
bool headless = false;
|
|
Optional<StringView> debug_process;
|
|
|
|
Core::ArgsParser args_parser;
|
|
args_parser.add_option(listen_address, "IP address to listen on", "listen-address", 'l', "listen_address");
|
|
args_parser.add_option(port, "Port to listen on", "port", 'p', "port");
|
|
args_parser.add_option(certificates, "Path to a certificate file", "certificate", 'C', "certificate");
|
|
args_parser.add_option(force_cpu_painting, "Launch browser with GPU painting disabled", "force-cpu-painting");
|
|
args_parser.add_option(debug_process, "Wait for a debugger to attach to the given process name (WebContent, RequestServer, etc.)", "debug-process", 0, "process-name");
|
|
args_parser.add_option(headless, "Launch browser without a graphical interface", "headless");
|
|
args_parser.parse(arguments);
|
|
|
|
auto ipv4_address = IPv4Address::from_string(listen_address);
|
|
if (!ipv4_address.has_value()) {
|
|
warnln("Invalid listen address: {}", listen_address);
|
|
return 1;
|
|
}
|
|
|
|
if ((u16)port != port) {
|
|
warnln("Invalid port number: {}", port);
|
|
return 1;
|
|
}
|
|
|
|
WebView::platform_init();
|
|
|
|
Web::WebDriver::set_default_interface_mode(headless ? Web::WebDriver::InterfaceMode::Headless : Web::WebDriver::InterfaceMode::Graphical);
|
|
|
|
auto webdriver_socket_path = ByteString::formatted("{}/webdriver", TRY(Core::StandardPaths::runtime_directory()));
|
|
TRY(Core::Directory::create(webdriver_socket_path, Core::Directory::CreateDirectories::Yes));
|
|
|
|
Core::EventLoop loop;
|
|
auto server = TRY(Core::TCPServer::try_create());
|
|
|
|
// FIXME: Propagate errors
|
|
server->on_ready_to_accept = [&] {
|
|
auto maybe_client_socket = server->accept();
|
|
if (maybe_client_socket.is_error()) {
|
|
warnln("Failed to accept the client: {}", maybe_client_socket.error());
|
|
return;
|
|
}
|
|
|
|
auto maybe_buffered_socket = Core::BufferedTCPSocket::create(maybe_client_socket.release_value());
|
|
if (maybe_buffered_socket.is_error()) {
|
|
warnln("Could not obtain a buffered socket for the client: {}", maybe_buffered_socket.error());
|
|
return;
|
|
}
|
|
|
|
auto launch_browser_callback = [&](ByteString const& socket_path, bool headless) {
|
|
auto arguments = create_arguments(socket_path, headless, force_cpu_painting, debug_process);
|
|
return launch_process("Ladybird"sv, arguments.span());
|
|
};
|
|
|
|
auto maybe_client = WebDriver::Client::try_create(maybe_buffered_socket.release_value(), move(launch_browser_callback), server);
|
|
if (maybe_client.is_error()) {
|
|
warnln("Could not create a WebDriver client: {}", maybe_client.error());
|
|
return;
|
|
}
|
|
};
|
|
|
|
TRY(server->listen(ipv4_address.value(), port, Core::TCPServer::AllowAddressReuse::Yes));
|
|
outln("Listening on {}:{}", ipv4_address.value(), port);
|
|
|
|
return loop.exec();
|
|
}
|