Ladybird: Implement WebDriver for Ladybird :^)

This adds a WebDriver binary for Ladybird to make use of Serenity's
WebDriver implementation. This has to use the same IPC socket handling
that was used to make WebContent work out-of-process. Besides that, we
are able to reuse almost everything from Serenity.
This commit is contained in:
Timothy Flynn 2022-11-14 12:09:14 -05:00 committed by Andrew Kaster
parent 54321f49ad
commit 9e0db602ca
Notes: sideshowbarker 2024-07-17 02:39:15 +09:00
6 changed files with 238 additions and 2 deletions

View file

@ -114,7 +114,8 @@ add_custom_target(debug
qt_finalize_executable(ladybird) qt_finalize_executable(ladybird)
add_subdirectory(WebContent) add_subdirectory(WebContent)
add_dependencies(ladybird WebContent) add_subdirectory(WebDriver)
add_dependencies(ladybird WebContent WebDriver)
if(NOT CMAKE_SKIP_INSTALL_RULES) if(NOT CMAKE_SKIP_INSTALL_RULES)
include(cmake/InstallRules.cmake) include(cmake/InstallRules.cmake)

View file

@ -587,7 +587,11 @@ void WebContentView::create_client()
MUST(Core::System::close(ui_fd_passing_fd)); MUST(Core::System::close(ui_fd_passing_fd));
MUST(Core::System::close(ui_fd)); MUST(Core::System::close(ui_fd));
auto takeover_string = String::formatted("WebContent:{}", wc_fd); String takeover_string;
if (auto* socket_takeover = getenv("SOCKET_TAKEOVER"))
takeover_string = String::formatted("{} WebContent:{}", socket_takeover, wc_fd);
else
takeover_string = String::formatted("WebContent:{}", wc_fd);
MUST(Core::System::setenv("SOCKET_TAKEOVER"sv, takeover_string, true)); MUST(Core::System::setenv("SOCKET_TAKEOVER"sv, takeover_string, true));
auto webcontent_fd_passing_socket_string = String::number(wc_fd_passing_fd); auto webcontent_fd_passing_socket_string = String::number(wc_fd_passing_fd);

View file

@ -0,0 +1,16 @@
set(WEBDRIVER_SOURCE_DIR ${SERENITY_SOURCE_DIR}/Userland/Services/WebDriver)
set(SOURCES
${WEBDRIVER_SOURCE_DIR}/Client.cpp
${WEBDRIVER_SOURCE_DIR}/WebContentConnection.cpp
Session.cpp
main.cpp
)
qt_add_executable(WebDriver ${SOURCES})
target_include_directories(WebDriver PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/..)
target_include_directories(WebDriver PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/..)
target_include_directories(WebDriver PRIVATE ${SERENITY_SOURCE_DIR}/Userland)
target_include_directories(WebDriver PRIVATE ${SERENITY_SOURCE_DIR}/Userland/Services)
target_link_libraries(WebDriver PRIVATE Qt::Core Qt::Network LibCore LibGfx LibIPC LibJS LibMain LibWeb LibWebSocket)

View file

@ -0,0 +1,104 @@
/*
* Copyright (c) 2022, Florent Castelli <florent.castelli@gmail.com>
* Copyright (c) 2022, Sam Atkins <atkinssj@serenityos.org>
* Copyright (c) 2022, Tobias Christiansen <tobyase@serenityos.org>
* Copyright (c) 2022, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2022, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#define AK_DONT_REPLACE_STD
#include "Session.h"
#include <LibCore/Stream.h>
#include <LibCore/System.h>
#include <WebDriver/Client.h>
#include <unistd.h>
namespace WebDriver {
Session::Session(unsigned session_id, NonnullRefPtr<Client> client)
: m_client(move(client))
, m_id(session_id)
{
}
Session::~Session()
{
if (auto error = stop(); error.is_error())
warnln("Failed to stop session {}: {}", m_id, error.error());
}
ErrorOr<void> Session::start()
{
int socket_fds[2] {};
TRY(Core::System::socketpair(AF_LOCAL, SOCK_STREAM, 0, socket_fds));
auto [webdriver_fd, webcontent_fd] = socket_fds;
int fd_passing_socket_fds[2] {};
TRY(Core::System::socketpair(AF_LOCAL, SOCK_STREAM, 0, fd_passing_socket_fds));
auto [webdriver_fd_passing_fd, webcontent_fd_passing_fd] = fd_passing_socket_fds;
m_browser_pid = TRY(Core::System::fork());
if (m_browser_pid == 0) {
TRY(Core::System::close(webdriver_fd_passing_fd));
TRY(Core::System::close(webdriver_fd));
auto takeover_string = String::formatted("WebDriver:{}", webcontent_fd);
TRY(Core::System::setenv("SOCKET_TAKEOVER"sv, takeover_string, true));
auto fd_passing_socket_string = String::number(webcontent_fd_passing_fd);
char const* argv[] = {
"ladybird",
"--webdriver-fd-passing-socket",
fd_passing_socket_string.characters(),
nullptr,
};
if (execvp("./ladybird", const_cast<char**>(argv)) < 0)
perror("execvp");
VERIFY_NOT_REACHED();
}
TRY(Core::System::close(webcontent_fd_passing_fd));
TRY(Core::System::close(webcontent_fd));
auto socket = TRY(Core::Stream::LocalSocket::adopt_fd(webdriver_fd));
TRY(socket->set_blocking(true));
m_web_content_connection = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) WebContentConnection(move(socket), m_client, session_id())));
m_web_content_connection->set_fd_passing_socket(TRY(Core::Stream::LocalSocket::adopt_fd(webdriver_fd_passing_fd)));
m_started = true;
return {};
}
// https://w3c.github.io/webdriver/#dfn-close-the-session
Web::WebDriver::Response Session::stop()
{
if (!m_started)
return JsonValue {};
// 1. Perform the following substeps based on the remote ends type:
// NOTE: We perform the "Remote end is an endpoint node" steps in the WebContent process.
m_web_content_connection->close_session();
// 2. Remove the current session from active sessions.
// NOTE: Handled by WebDriver::Client.
// 3. Perform any implementation-specific cleanup steps.
if (m_browser_pid.has_value()) {
MUST(Core::System::kill(*m_browser_pid, SIGTERM));
m_browser_pid = {};
}
m_started = false;
// 4. If an error has occurred in any of the steps above, return the error, otherwise return success with data null.
return JsonValue {};
}
}

View file

@ -0,0 +1,44 @@
/*
* Copyright (c) 2022, Florent Castelli <florent.castelli@gmail.com>
* Copyright (c) 2022, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2022, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Error.h>
#include <AK/RefPtr.h>
#include <LibWeb/WebDriver/Error.h>
#include <LibWeb/WebDriver/Response.h>
#include <WebDriver/WebContentConnection.h>
#include <unistd.h>
namespace WebDriver {
class Session {
public:
Session(unsigned session_id, NonnullRefPtr<Client> client);
~Session();
unsigned session_id() const { return m_id; }
WebContentConnection& web_content_connection()
{
VERIFY(m_web_content_connection);
return *m_web_content_connection;
}
ErrorOr<void> start();
Web::WebDriver::Response stop();
private:
NonnullRefPtr<Client> m_client;
bool m_started { false };
unsigned m_id { 0 };
RefPtr<WebContentConnection> m_web_content_connection;
Optional<pid_t> m_browser_pid;
};
}

View file

@ -0,0 +1,67 @@
/*
* Copyright (c) 2022, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#define AK_DONT_REPLACE_STD
#include <LibCore/ArgsParser.h>
#include <LibCore/EventLoop.h>
#include <LibCore/System.h>
#include <LibCore/TCPServer.h>
#include <LibMain/Main.h>
#include <WebDriver/Client.h>
extern String s_serenity_resource_root;
ErrorOr<int> serenity_main(Main::Arguments arguments)
{
auto listen_address = "0.0.0.0"sv;
int port = 8000;
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.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;
}
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::Stream::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 maybe_client = WebDriver::Client::try_create(maybe_buffered_socket.release_value(), 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();
}