diff --git a/Ladybird/CMakeLists.txt b/Ladybird/CMakeLists.txt index 03bffa1a58c..d8151c22c53 100644 --- a/Ladybird/CMakeLists.txt +++ b/Ladybird/CMakeLists.txt @@ -114,7 +114,8 @@ add_custom_target(debug qt_finalize_executable(ladybird) add_subdirectory(WebContent) -add_dependencies(ladybird WebContent) +add_subdirectory(WebDriver) +add_dependencies(ladybird WebContent WebDriver) if(NOT CMAKE_SKIP_INSTALL_RULES) include(cmake/InstallRules.cmake) diff --git a/Ladybird/WebContentView.cpp b/Ladybird/WebContentView.cpp index 82e3f180961..dc8c3655578 100644 --- a/Ladybird/WebContentView.cpp +++ b/Ladybird/WebContentView.cpp @@ -587,7 +587,11 @@ void WebContentView::create_client() MUST(Core::System::close(ui_fd_passing_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)); auto webcontent_fd_passing_socket_string = String::number(wc_fd_passing_fd); diff --git a/Ladybird/WebDriver/CMakeLists.txt b/Ladybird/WebDriver/CMakeLists.txt new file mode 100644 index 00000000000..69601b1e81b --- /dev/null +++ b/Ladybird/WebDriver/CMakeLists.txt @@ -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) diff --git a/Ladybird/WebDriver/Session.cpp b/Ladybird/WebDriver/Session.cpp new file mode 100644 index 00000000000..b3f29348b19 --- /dev/null +++ b/Ladybird/WebDriver/Session.cpp @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2022, Florent Castelli + * Copyright (c) 2022, Sam Atkins + * Copyright (c) 2022, Tobias Christiansen + * Copyright (c) 2022, Linus Groh + * Copyright (c) 2022, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#define AK_DONT_REPLACE_STD + +#include "Session.h" +#include +#include +#include +#include + +namespace WebDriver { + +Session::Session(unsigned session_id, NonnullRefPtr 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 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(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 {}; +} + +} diff --git a/Ladybird/WebDriver/Session.h b/Ladybird/WebDriver/Session.h new file mode 100644 index 00000000000..601692351cd --- /dev/null +++ b/Ladybird/WebDriver/Session.h @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022, Florent Castelli + * Copyright (c) 2022, Linus Groh + * Copyright (c) 2022, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace WebDriver { + +class Session { +public: + Session(unsigned session_id, NonnullRefPtr client); + ~Session(); + + unsigned session_id() const { return m_id; } + + WebContentConnection& web_content_connection() + { + VERIFY(m_web_content_connection); + return *m_web_content_connection; + } + + ErrorOr start(); + Web::WebDriver::Response stop(); + +private: + NonnullRefPtr m_client; + bool m_started { false }; + unsigned m_id { 0 }; + RefPtr m_web_content_connection; + Optional m_browser_pid; +}; + +} diff --git a/Ladybird/WebDriver/main.cpp b/Ladybird/WebDriver/main.cpp new file mode 100644 index 00000000000..ef949e960f7 --- /dev/null +++ b/Ladybird/WebDriver/main.cpp @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2022, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#define AK_DONT_REPLACE_STD + +#include +#include +#include +#include +#include +#include + +extern String s_serenity_resource_root; + +ErrorOr 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(); +}