ladybird/Libraries/LibWebView/HelperProcess.cpp
Timothy Flynn c011dc766f LibWebView+WebDriver+UI: Migrate headless browsing to main Ladybird exe
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.
2025-06-10 12:04:59 -04:00

262 lines
10 KiB
C++

/*
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Enumerate.h>
#include <LibCore/Process.h>
#include <LibWebView/Application.h>
#include <LibWebView/HelperProcess.h>
#include <LibWebView/Utilities.h>
namespace WebView {
template<typename ClientType, typename... ClientArguments>
static ErrorOr<NonnullRefPtr<ClientType>> launch_server_process(
StringView server_name,
Vector<ByteString> arguments,
ClientArguments&&... client_arguments)
{
auto process_type = WebView::process_type_from_name(server_name);
auto const& browser_options = WebView::Application::browser_options();
auto candidate_server_paths = TRY(get_paths_for_helper_process(server_name));
if (browser_options.profile_helper_process == process_type) {
arguments.prepend({
"--tool=callgrind"sv,
"--instr-atstart=no"sv,
""sv, // Placeholder for the process path.
});
}
if (browser_options.debug_helper_process == process_type)
arguments.append("--wait-for-debugger"sv);
for (auto [i, path] : enumerate(candidate_server_paths)) {
Core::ProcessSpawnOptions options { .name = server_name, .arguments = arguments };
if (browser_options.profile_helper_process == process_type) {
options.executable = "valgrind"sv;
options.search_for_executable_in_path = true;
arguments[2] = path;
} else {
options.executable = path;
}
auto result = WebView::Process::spawn<ClientType>(process_type, move(options), forward<ClientArguments>(client_arguments)...);
if (!result.is_error()) {
auto&& [process, client] = result.release_value();
if constexpr (requires { client->set_pid(pid_t {}); })
client->set_pid(process.pid());
if constexpr (requires { client->transport().set_peer_pid(0); } && !IsSame<ClientType, Web::HTML::WebWorkerClient>) {
auto response = client->template send_sync<typename ClientType::InitTransport>(Core::System::getpid());
client->transport().set_peer_pid(response->peer_pid());
}
WebView::Application::the().add_child_process(move(process));
if (browser_options.profile_helper_process == process_type) {
dbgln();
dbgln("\033[1;34mLaunched {} process under callgrind!\033[0m", server_name);
dbgln("\033[1;36mRun `\033[4mcallgrind_control -i on\033[24m` to start instrumentation and `\033[4mcallgrind_control -i off\033[24m` stop it again.\033[0m");
dbgln();
}
return move(client);
}
if (i == candidate_server_paths.size() - 1) {
warnln("Could not launch any of {}: {}", candidate_server_paths, result.error());
return result.release_error();
}
}
VERIFY_NOT_REACHED();
}
template<typename... ClientArguments>
static ErrorOr<NonnullRefPtr<WebView::WebContentClient>> launch_web_content_process_impl(
IPC::File image_decoder_socket,
Optional<IPC::File> request_server_socket,
ClientArguments&&... client_arguments)
{
auto const& browser_options = WebView::Application::browser_options();
auto const& web_content_options = WebView::Application::web_content_options();
Vector<ByteString> arguments {
"--command-line"sv,
web_content_options.command_line.to_byte_string(),
"--executable-path"sv,
web_content_options.executable_path.to_byte_string(),
};
if (browser_options.headless_mode.has_value())
arguments.append("--headless"sv);
if (web_content_options.config_path.has_value()) {
arguments.append("--config-path"sv);
arguments.append(web_content_options.config_path.value());
}
if (web_content_options.is_layout_test_mode == WebView::IsLayoutTestMode::Yes)
arguments.append("--layout-test-mode"sv);
if (web_content_options.log_all_js_exceptions == WebView::LogAllJSExceptions::Yes)
arguments.append("--log-all-js-exceptions"sv);
if (web_content_options.disable_site_isolation == WebView::DisableSiteIsolation::Yes)
arguments.append("--disable-site-isolation"sv);
if (web_content_options.enable_idl_tracing == WebView::EnableIDLTracing::Yes)
arguments.append("--enable-idl-tracing"sv);
if (web_content_options.enable_http_cache == WebView::EnableHTTPCache::Yes)
arguments.append("--enable-http-cache"sv);
if (web_content_options.expose_internals_object == WebView::ExposeInternalsObject::Yes)
arguments.append("--expose-internals-object"sv);
if (web_content_options.force_cpu_painting == WebView::ForceCPUPainting::Yes)
arguments.append("--force-cpu-painting"sv);
if (web_content_options.force_fontconfig == WebView::ForceFontconfig::Yes)
arguments.append("--force-fontconfig"sv);
if (web_content_options.collect_garbage_on_every_allocation == WebView::CollectGarbageOnEveryAllocation::Yes)
arguments.append("--collect-garbage-on-every-allocation"sv);
if (web_content_options.paint_viewport_scrollbars == PaintViewportScrollbars::No)
arguments.append("--disable-scrollbar-painting"sv);
if (auto const maybe_echo_server_port = web_content_options.echo_server_port; maybe_echo_server_port.has_value()) {
arguments.append("--echo-server-port"sv);
arguments.append(ByteString::number(maybe_echo_server_port.value()));
}
if (auto server = mach_server_name(); server.has_value()) {
arguments.append("--mach-server-name"sv);
arguments.append(server.value());
}
if (request_server_socket.has_value()) {
arguments.append("--request-server-socket"sv);
arguments.append(ByteString::number(request_server_socket->fd()));
}
arguments.append("--image-decoder-socket"sv);
arguments.append(ByteString::number(image_decoder_socket.fd()));
return launch_server_process<WebView::WebContentClient>("WebContent"sv, move(arguments), forward<ClientArguments>(client_arguments)...);
}
ErrorOr<NonnullRefPtr<WebView::WebContentClient>> launch_web_content_process(
WebView::ViewImplementation& view,
IPC::File image_decoder_socket,
Optional<IPC::File> request_server_socket)
{
return launch_web_content_process_impl(move(image_decoder_socket), move(request_server_socket), view);
}
ErrorOr<NonnullRefPtr<WebView::WebContentClient>> launch_spare_web_content_process(
IPC::File image_decoder_socket,
Optional<IPC::File> request_server_socket)
{
return launch_web_content_process_impl(move(image_decoder_socket), move(request_server_socket));
}
ErrorOr<NonnullRefPtr<ImageDecoderClient::Client>> launch_image_decoder_process()
{
Vector<ByteString> arguments;
if (auto server = mach_server_name(); server.has_value()) {
arguments.append("--mach-server-name"sv);
arguments.append(server.value());
}
return launch_server_process<ImageDecoderClient::Client>("ImageDecoder"sv, arguments);
}
ErrorOr<NonnullRefPtr<Web::HTML::WebWorkerClient>> launch_web_worker_process(Web::Bindings::AgentType type)
{
Vector<ByteString> arguments;
auto request_server_socket = TRY(connect_new_request_server_client());
arguments.append("--request-server-socket"sv);
arguments.append(ByteString::number(request_server_socket.fd()));
auto image_decoder_socket = TRY(connect_new_image_decoder_client());
arguments.append("--image-decoder-socket"sv);
arguments.append(ByteString::number(image_decoder_socket.fd()));
arguments.append("--type"sv);
switch (type) {
case Web::Bindings::AgentType::DedicatedWorker:
arguments.append("dedicated"sv);
break;
case Web::Bindings::AgentType::SharedWorker:
arguments.append("shared"sv);
break;
case Web::Bindings::AgentType::ServiceWorker:
arguments.append("service"sv);
break;
default:
VERIFY_NOT_REACHED();
}
return launch_server_process<Web::HTML::WebWorkerClient>("WebWorker"sv, move(arguments));
}
ErrorOr<NonnullRefPtr<Requests::RequestClient>> launch_request_server_process()
{
Vector<ByteString> arguments;
if (!s_ladybird_resource_root.is_empty()) {
arguments.append("--serenity-resource-root"sv);
arguments.append(s_ladybird_resource_root);
}
for (auto const& certificate : WebView::Application::browser_options().certificates)
arguments.append(ByteString::formatted("--certificate={}", certificate));
if (auto server = mach_server_name(); server.has_value()) {
arguments.append("--mach-server-name"sv);
arguments.append(server.value());
}
auto client = TRY(launch_server_process<Requests::RequestClient>("RequestServer"sv, move(arguments)));
WebView::Application::settings().dns_settings().visit(
[](WebView::SystemDNS) {},
[&](WebView::DNSOverTLS const& dns_over_tls) {
dbgln("Setting DNS server to {}:{} with TLS", dns_over_tls.server_address, dns_over_tls.port);
client->async_set_dns_server(dns_over_tls.server_address, dns_over_tls.port, true);
},
[&](WebView::DNSOverUDP const& dns_over_udp) {
dbgln("Setting DNS server to {}:{}", dns_over_udp.server_address, dns_over_udp.port);
client->async_set_dns_server(dns_over_udp.server_address, dns_over_udp.port, false);
});
return client;
}
ErrorOr<IPC::File> connect_new_request_server_client()
{
auto new_socket = Application::request_server_client().send_sync_but_allow_failure<Messages::RequestServer::ConnectNewClient>();
if (!new_socket)
return Error::from_string_literal("Failed to connect to RequestServer");
auto socket = new_socket->take_client_socket();
TRY(socket.clear_close_on_exec());
return socket;
}
ErrorOr<IPC::File> connect_new_image_decoder_client()
{
auto new_socket = Application::image_decoder_client().send_sync_but_allow_failure<Messages::ImageDecoderServer::ConnectNewClients>(1);
if (!new_socket)
return Error::from_string_literal("Failed to connect to ImageDecoder");
auto sockets = new_socket->take_sockets();
if (sockets.size() != 1)
return Error::from_string_literal("Failed to connect to ImageDecoder");
auto socket = sockets.take_last();
TRY(socket.clear_close_on_exec());
return socket;
}
}