From e840d27d8efbcb980507e64b677873b503b6d033 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Mon, 21 Nov 2022 20:03:45 -0500 Subject: [PATCH] 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. --- Meta/Lagom/CMakeLists.txt | 4 +- Userland/Utilities/CMakeLists.txt | 5 +- Userland/Utilities/headless-browser.cpp | 92 ++++++++++++++++++------- 3 files changed, 74 insertions(+), 27 deletions(-) diff --git a/Meta/Lagom/CMakeLists.txt b/Meta/Lagom/CMakeLists.txt index a0e659bb328..5b9730a77dc 100644 --- a/Meta/Lagom/CMakeLists.txt +++ b/Meta/Lagom/CMakeLists.txt @@ -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) diff --git a/Userland/Utilities/CMakeLists.txt b/Userland/Utilities/CMakeLists.txt index f56eaa66ae3..489079d20c2 100644 --- a/Userland/Utilities/CMakeLists.txt +++ b/Userland/Utilities/CMakeLists.txt @@ -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") diff --git a/Userland/Utilities/headless-browser.cpp b/Userland/Utilities/headless-browser.cpp index 82c95c59ad7..75f23cbee00 100644 --- a/Userland/Utilities/headless-browser.cpp +++ b/Userland/Utilities/headless-browser.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -47,6 +48,7 @@ #include #include #include +#include class HeadlessBrowserPageClient final : public Web::PageClient { public: @@ -107,9 +109,30 @@ public: m_screen_rect = screen_rect; } + ErrorOr connect_to_webdriver(StringView webdriver_ipc_path) + { + VERIFY(!m_webdriver); + m_webdriver = TRY(WebContent::WebDriverConnection::connect(*this, webdriver_ipc_path)); + return {}; + } + + ErrorOr 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 m_palette_impl; Gfx::IntRect m_screen_rect { 0, 0, 800, 600 }; Web::CSS::PreferredColorScheme m_preferred_color_scheme { Web::CSS::PreferredColorScheme::Auto }; + + RefPtr 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 serenity_main(Main::Arguments arguments) { int take_screenshot_after = 1; @@ -664,6 +719,8 @@ ErrorOr 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 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 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(); }