mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-11-22 07:30:19 +00:00
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:
parent
54321f49ad
commit
9e0db602ca
Notes:
sideshowbarker
2024-07-17 02:39:15 +09:00
Author: https://github.com/trflynn89 Commit: https://github.com/SerenityOS/serenity/commit/9e0db602ca Pull-request: https://github.com/SerenityOS/serenity/pull/16583 Reviewed-by: https://github.com/ADKaster Reviewed-by: https://github.com/awesomekling ✅ Reviewed-by: https://github.com/linusg
6 changed files with 238 additions and 2 deletions
|
@ -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)
|
||||||
|
|
|
@ -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);
|
||||||
|
|
16
Ladybird/WebDriver/CMakeLists.txt
Normal file
16
Ladybird/WebDriver/CMakeLists.txt
Normal 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)
|
104
Ladybird/WebDriver/Session.cpp
Normal file
104
Ladybird/WebDriver/Session.cpp
Normal 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 end’s 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 {};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
44
Ladybird/WebDriver/Session.h
Normal file
44
Ladybird/WebDriver/Session.h
Normal 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
67
Ladybird/WebDriver/main.cpp
Normal file
67
Ladybird/WebDriver/main.cpp
Normal 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();
|
||||||
|
}
|
Loading…
Reference in a new issue