headless-browser: Add a mode for being controlled by WebDriver

This adds command line flags for WebDriver to pass its IPC socket path
(if running on Serenity) or its FD passing socket (if running elsewhere)
for the headless-browser to connect to.
This commit is contained in:
Timothy Flynn 2022-11-21 20:03:45 -05:00 committed by Linus Groh
parent 0135a2ab5b
commit e840d27d8e
Notes: sideshowbarker 2024-07-17 04:41:05 +09:00
3 changed files with 74 additions and 27 deletions

View File

@ -460,8 +460,8 @@ if (BUILD_LAGOM)
target_link_libraries(gml-format LibCore LibGUI LibMain)
if (ENABLE_LAGOM_LIBWEB)
add_executable(headless-browser ../../Userland/Utilities/headless-browser.cpp)
target_link_libraries(headless-browser LibWeb LibWebSocket LibCrypto LibGemini LibHTTP LibJS LibGfx LibMain LibTLS)
add_executable(headless-browser ../../Userland/Utilities/headless-browser.cpp ../../Userland/Services/WebContent/WebDriverConnection.cpp)
target_link_libraries(headless-browser LibWeb LibWebSocket LibCrypto LibGemini LibHTTP LibJS LibGfx LibMain LibTLS LibIPC LibJS)
endif()
add_executable(js ../../Userland/Utilities/js.cpp)

View File

@ -89,7 +89,7 @@ target_link_libraries(gml-format PRIVATE LibGUI)
target_link_libraries(grep PRIVATE LibRegex)
target_link_libraries(gunzip PRIVATE LibCompress)
target_link_libraries(gzip PRIVATE LibCompress)
target_link_libraries(headless-browser PRIVATE LibCrypto LibGemini LibGfx LibHTTP LibTLS LibWeb LibWebSocket)
target_link_libraries(headless-browser PRIVATE LibCrypto LibGemini LibGfx LibHTTP LibTLS LibWeb LibWebSocket LibIPC LibJS)
target_link_libraries(jail-attach PRIVATE LibCore LibMain)
target_link_libraries(jail-create PRIVATE LibCore LibMain)
target_link_libraries(js PRIVATE LibCrypto LibJS LibLine LibLocale LibTextCodec)
@ -128,3 +128,6 @@ target_link_libraries(wasm PRIVATE LibWasm LibLine)
target_link_libraries(wsctl PRIVATE LibGUI LibIPC)
target_link_libraries(xml PRIVATE LibXML)
target_link_libraries(zip PRIVATE LibArchive LibCompress LibCrypto)
# FIXME: Link this file into headless-browser without compiling it again.
target_sources(headless-browser PRIVATE "${SerenityOS_SOURCE_DIR}/Userland/Services/WebContent/WebDriverConnection.cpp")

View File

@ -19,6 +19,7 @@
#include <LibCore/MemoryStream.h>
#include <LibCore/Stream.h>
#include <LibCore/System.h>
#include <LibCore/SystemServerTakeover.h>
#include <LibCore/Timer.h>
#include <LibGemini/GeminiRequest.h>
#include <LibGemini/GeminiResponse.h>
@ -47,6 +48,7 @@
#include <LibWebSocket/ConnectionInfo.h>
#include <LibWebSocket/Message.h>
#include <LibWebSocket/WebSocket.h>
#include <WebContent/WebDriverConnection.h>
class HeadlessBrowserPageClient final : public Web::PageClient {
public:
@ -107,9 +109,30 @@ public:
m_screen_rect = screen_rect;
}
ErrorOr<void> connect_to_webdriver(StringView webdriver_ipc_path)
{
VERIFY(!m_webdriver);
m_webdriver = TRY(WebContent::WebDriverConnection::connect(*this, webdriver_ipc_path));
return {};
}
ErrorOr<void> connect_to_webdriver(int webdriver_fd_passing_socket)
{
VERIFY(!m_webdriver);
VERIFY(webdriver_fd_passing_socket >= 0);
auto socket = TRY(Core::take_over_socket_from_system_server("WebDriver"sv));
m_webdriver = TRY(WebContent::WebDriverConnection::try_create(move(socket), *this));
m_webdriver->set_fd_passing_socket(TRY(Core::Stream::LocalSocket::adopt_fd(webdriver_fd_passing_socket)));
return {};
}
// ^Web::PageClient
virtual bool is_connection_open() const override
{
if (m_webdriver)
return m_webdriver->is_open();
return true;
}
@ -238,6 +261,8 @@ private:
RefPtr<Gfx::PaletteImpl> m_palette_impl;
Gfx::IntRect m_screen_rect { 0, 0, 800, 600 };
Web::CSS::PreferredColorScheme m_preferred_color_scheme { Web::CSS::PreferredColorScheme::Auto };
RefPtr<WebContent::WebDriverConnection> m_webdriver;
};
class ImageCodecPluginHeadless : public Web::Platform::ImageCodecPlugin {
@ -657,6 +682,36 @@ private:
HeadlessWebSocketClientManager() { }
};
static void load_page_for_screenshot_and_exit(HeadlessBrowserPageClient& page_client, int take_screenshot_after)
{
dbgln("Taking screenshot after {} seconds", take_screenshot_after);
auto timer = Core::Timer::create_single_shot(
take_screenshot_after * 1000,
[&]() {
// FIXME: Allow passing the output path as argument
String output_file_path = "output.png";
dbgln("Saving to {}", output_file_path);
if (Core::File::exists(output_file_path))
MUST(Core::File::remove(output_file_path, Core::File::RecursionMode::Disallowed, true));
auto output_file = MUST(Core::Stream::File::open(output_file_path, Core::Stream::OpenMode::Write));
auto output_rect = page_client.screen_rect();
auto output_bitmap = MUST(Gfx::Bitmap::try_create(Gfx::BitmapFormat::BGRx8888, output_rect.size()));
page_client.paint(output_rect, output_bitmap);
auto image_buffer = Gfx::PNGWriter::encode(output_bitmap);
MUST(output_file->write(image_buffer.bytes()));
exit(0);
});
timer->start();
}
ErrorOr<int> serenity_main(Main::Arguments arguments)
{
int take_screenshot_after = 1;
@ -664,6 +719,8 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
StringView resources_folder;
StringView error_page_url;
StringView ca_certs_path;
StringView webdriver_ipc_path;
int webdriver_fd_passing_socket { -1 };
Core::EventLoop event_loop;
Core::ArgsParser args_parser;
@ -672,9 +729,14 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
args_parser.add_option(resources_folder, "Path of the base resources folder (defaults to /res)", "resources", 'r', "resources-root-path");
args_parser.add_option(error_page_url, "URL for the error page (defaults to file:///res/html/error.html)", "error-page", 'e', "error-page-url");
args_parser.add_option(ca_certs_path, "The bundled ca certificates file", "certs", 'c', "ca-certs-path");
args_parser.add_option(webdriver_ipc_path, "Path to the WebDriver IPC socket", "webdriver-ipc-path", 0, "path");
args_parser.add_option(webdriver_fd_passing_socket, "File descriptor of the passing socket for the WebDriver connection", "webdriver-fd-passing-socket", 'd', "webdriver_fd_passing_socket");
args_parser.add_positional_argument(url, "URL to open", "url", Core::ArgsParser::Required::Yes);
args_parser.parse(arguments);
if (!webdriver_ipc_path.is_empty() && webdriver_fd_passing_socket >= 0)
return Error::from_string_view("Only one of --webdriver-ipc-path and --webdriver-fd-passing-socket may be used"sv);
Web::Platform::EventLoopPlugin::install(*new Web::Platform::EventLoopPluginSerenity);
Web::Platform::FontPlugin::install(*new Web::Platform::FontPluginSerenity);
Web::Platform::ImageCodecPlugin::install(*new ImageCodecPluginHeadless);
@ -716,30 +778,12 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
page_client->set_viewport_rect({ 0, 0, 800, 600 });
page_client->set_screen_rect({ 0, 0, 800, 600 });
dbgln("Taking screenshot after {} seconds !", take_screenshot_after);
auto timer = Core::Timer::create_single_shot(
take_screenshot_after * 1000,
[page_client = move(page_client)] {
// FIXME: Allow passing the output path as argument
String output_file_path = "output.png";
dbgln("Saving to {}", output_file_path);
if (Core::File::exists(output_file_path))
[[maybe_unused]]
auto ignored = Core::File::remove(output_file_path, Core::File::RecursionMode::Disallowed, true);
auto output_file = MUST(Core::Stream::File::open(output_file_path, Core::Stream::OpenMode::Write));
auto output_rect = page_client->screen_rect();
auto output_bitmap = MUST(Gfx::Bitmap::try_create(Gfx::BitmapFormat::BGRx8888, output_rect.size()));
page_client->paint(output_rect, output_bitmap);
auto image_buffer = Gfx::PNGWriter::encode(output_bitmap);
MUST(output_file->write(image_buffer.bytes()));
exit(0);
});
timer->start();
if (!webdriver_ipc_path.is_empty())
TRY(page_client->connect_to_webdriver(webdriver_ipc_path));
else if (webdriver_fd_passing_socket >= 0)
TRY(page_client->connect_to_webdriver(webdriver_fd_passing_socket));
else
load_page_for_screenshot_and_exit(*page_client, take_screenshot_after);
return event_loop.exec();
}