diff --git a/AK/Debug.h.in b/AK/Debug.h.in index 29b106f34c1..f661e850bef 100644 --- a/AK/Debug.h.in +++ b/AK/Debug.h.in @@ -50,6 +50,10 @@ # cmakedefine01 CSS_TRANSITIONS_DEBUG #endif +#ifndef DEVTOOLS_DEBUG +# cmakedefine01 DEVTOOLS_DEBUG +#endif + #ifndef DNS_DEBUG # cmakedefine01 DNS_DEBUG #endif diff --git a/Libraries/LibDevTools/Actor.cpp b/Libraries/LibDevTools/Actor.cpp new file mode 100644 index 00000000000..57af75e9330 --- /dev/null +++ b/Libraries/LibDevTools/Actor.cpp @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include + +namespace DevTools { + +Actor::Actor(DevToolsServer& devtools, ByteString name) + : m_devtools(devtools) + , m_name(move(name)) +{ +} + +Actor::~Actor() = default; + +void Actor::send_message(JsonValue message, Optional block_token) +{ + if (m_block_responses && !block_token.has_value()) { + m_blocked_responses.append(move(message)); + return; + } + + if (auto& connection = devtools().connection()) + connection->send_message(message); +} + +// https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#error-packets +void Actor::send_missing_parameter_error(StringView parameter) +{ + JsonObject error; + error.set("from"sv, name()); + error.set("error"sv, "missingParameter"sv); + error.set("message"sv, ByteString::formatted("Missing parameter: '{}'", parameter)); + send_message(move(error)); +} + +// https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#error-packets +void Actor::send_unrecognized_packet_type_error(StringView type) +{ + JsonObject error; + error.set("from"sv, name()); + error.set("error"sv, "unrecognizedPacketType"sv); + error.set("message"sv, ByteString::formatted("Unrecognized packet type: '{}'", type)); + send_message(move(error)); +} + +// https://github.com/mozilla/gecko-dev/blob/master/devtools/server/actors/object.js +// This error is not documented, but is used by Firefox nonetheless. +void Actor::send_unknown_actor_error(StringView actor) +{ + JsonObject error; + error.set("from"sv, name()); + error.set("error"sv, "unknownActor"sv); + error.set("message"sv, ByteString::formatted("Unknown actor: '{}'", actor)); + send_message(move(error)); +} + +Actor::BlockToken Actor::block_responses() +{ + // https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#the-request-reply-pattern + // The actor processes packets in the order they are received, and the client can trust that the i’th reply + // corresponds to the i’th request. + + // The above requirement gets tricky for actors which require an async implementation. For example, the "getWalker" + // message sent to the InspectorActor results in the server fetching the DOM tree as JSON from the WebContent process. + // We cannot reply to the message until that is received. However, we will likely receive more messages from the + // client in that time. We cannot reply to those messages until we've replied to the "getWalker" message. Thus, we + // use this token to queue responses from the actor until that reply can be sent. + return { {}, *this }; +} + +Actor::BlockToken::BlockToken(Badge, Actor& actor) + : m_actor(actor) +{ + // If we end up in a situtation where an actor has multiple async handlers at once, we will need to come up with a + // more sophisticated blocking mechanism. + VERIFY(!actor.m_block_responses); + actor.m_block_responses = true; +} + +Actor::BlockToken::BlockToken(BlockToken&& other) + : m_actor(move(other.m_actor)) +{ +} + +Actor::BlockToken& Actor::BlockToken::operator=(BlockToken&& other) +{ + m_actor = move(other.m_actor); + return *this; +} + +Actor::BlockToken::~BlockToken() +{ + auto actor = m_actor.strong_ref(); + if (!actor) + return; + + auto blocked_responses = move(actor->m_blocked_responses); + actor->m_block_responses = false; + + for (auto& message : blocked_responses) + actor->send_message(move(message)); +} + +} diff --git a/Libraries/LibDevTools/Actor.h b/Libraries/LibDevTools/Actor.h new file mode 100644 index 00000000000..0b8728ad8d6 --- /dev/null +++ b/Libraries/LibDevTools/Actor.h @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace DevTools { + +class Actor + : public RefCounted + , public Weakable { +public: + virtual ~Actor(); + + ByteString const& name() const { return m_name; } + virtual void handle_message(StringView type, JsonObject const&) = 0; + + class [[nodiscard]] BlockToken { + public: + BlockToken(Badge, Actor&); + ~BlockToken(); + + BlockToken(BlockToken const&) = delete; + BlockToken& operator=(BlockToken const&) = delete; + + BlockToken(BlockToken&&); + BlockToken& operator=(BlockToken&&); + + private: + WeakPtr m_actor; + }; + + void send_message(JsonValue, Optional block_token = {}); + void send_missing_parameter_error(StringView parameter); + void send_unrecognized_packet_type_error(StringView type); + void send_unknown_actor_error(StringView actor); + +protected: + explicit Actor(DevToolsServer&, ByteString name); + + DevToolsServer& devtools() { return m_devtools; } + DevToolsServer const& devtools() const { return m_devtools; } + + BlockToken block_responses(); + +private: + DevToolsServer& m_devtools; + ByteString m_name; + + Vector m_blocked_responses; + bool m_block_responses { false }; +}; + +} diff --git a/Libraries/LibDevTools/Actors/DeviceActor.cpp b/Libraries/LibDevTools/Actors/DeviceActor.cpp new file mode 100644 index 00000000000..858fd4a919c --- /dev/null +++ b/Libraries/LibDevTools/Actors/DeviceActor.cpp @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include + +namespace DevTools { + +NonnullRefPtr DeviceActor::create(DevToolsServer& devtools, ByteString name) +{ + return adopt_ref(*new DeviceActor(devtools, move(name))); +} + +DeviceActor::DeviceActor(DevToolsServer& devtools, ByteString name) + : Actor(devtools, move(name)) +{ +} + +DeviceActor::~DeviceActor() = default; + +void DeviceActor::handle_message(StringView type, JsonObject const&) +{ + if (type == "getDescription"sv) { + auto build_id = Core::Version::read_long_version_string().to_byte_string(); + + // https://github.com/mozilla/gecko-dev/blob/master/devtools/shared/system.js + JsonObject value; + value.set("apptype"sv, "ladybird"sv); + value.set("name"sv, BROWSER_NAME); + value.set("brandName"sv, BROWSER_NAME); + value.set("version"sv, BROWSER_VERSION); + value.set("appbuildid"sv, build_id); + value.set("platformbuildid"sv, build_id); + value.set("platformversion"sv, "135.0"sv); + value.set("useragent"sv, Web::default_user_agent); + value.set("os"sv, OS_STRING); + value.set("arch"sv, CPU_STRING); + + JsonObject response; + response.set("from"sv, name()); + response.set("value"sv, move(value)); + + send_message(move(response)); + return; + } + + send_unrecognized_packet_type_error(type); +} + +} diff --git a/Libraries/LibDevTools/Actors/DeviceActor.h b/Libraries/LibDevTools/Actors/DeviceActor.h new file mode 100644 index 00000000000..7f97a050a9b --- /dev/null +++ b/Libraries/LibDevTools/Actors/DeviceActor.h @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace DevTools { + +class DeviceActor final : public Actor { +public: + static constexpr auto base_name = "device"sv; + + static NonnullRefPtr create(DevToolsServer&, ByteString name); + virtual ~DeviceActor() override; + + virtual void handle_message(StringView type, JsonObject const&) override; + +private: + DeviceActor(DevToolsServer&, ByteString name); +}; + +} diff --git a/Libraries/LibDevTools/Actors/PreferenceActor.cpp b/Libraries/LibDevTools/Actors/PreferenceActor.cpp new file mode 100644 index 00000000000..d4097d67aac --- /dev/null +++ b/Libraries/LibDevTools/Actors/PreferenceActor.cpp @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include + +namespace DevTools { + +NonnullRefPtr PreferenceActor::create(DevToolsServer& devtools, ByteString name) +{ + return adopt_ref(*new PreferenceActor(devtools, move(name))); +} + +PreferenceActor::PreferenceActor(DevToolsServer& devtools, ByteString name) + : Actor(devtools, move(name)) +{ +} + +PreferenceActor::~PreferenceActor() = default; + +void PreferenceActor::handle_message(StringView type, JsonObject const&) +{ + // FIXME: During session initialization, Firefox DevTools asks for the following boolean configurations: + // browser.privatebrowsing.autostart + // devtools.debugger.prompt-connection + // dom.serviceWorkers.enabled + // We just blindly return `false` for these, but we will eventually want a real configuration manager. + if (type == "getBoolPref"sv) { + JsonObject response; + response.set("from"sv, name()); + response.set("value"sv, false); + send_message(move(response)); + return; + } + + send_unrecognized_packet_type_error(type); +} + +} diff --git a/Libraries/LibDevTools/Actors/PreferenceActor.h b/Libraries/LibDevTools/Actors/PreferenceActor.h new file mode 100644 index 00000000000..15bb6b6ce1c --- /dev/null +++ b/Libraries/LibDevTools/Actors/PreferenceActor.h @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace DevTools { + +class PreferenceActor final : public Actor { +public: + static constexpr auto base_name = "preference"sv; + + static NonnullRefPtr create(DevToolsServer&, ByteString name); + virtual ~PreferenceActor() override; + + virtual void handle_message(StringView type, JsonObject const&) override; + +private: + PreferenceActor(DevToolsServer&, ByteString name); +}; + +} diff --git a/Libraries/LibDevTools/Actors/ProcessActor.cpp b/Libraries/LibDevTools/Actors/ProcessActor.cpp new file mode 100644 index 00000000000..4b14cb4a891 --- /dev/null +++ b/Libraries/LibDevTools/Actors/ProcessActor.cpp @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include + +namespace DevTools { + +NonnullRefPtr ProcessActor::create(DevToolsServer& devtools, ByteString name, ProcessDescription description) +{ + return adopt_ref(*new ProcessActor(devtools, move(name), move(description))); +} + +ProcessActor::ProcessActor(DevToolsServer& devtools, ByteString name, ProcessDescription description) + : Actor(devtools, move(name)) + , m_description(move(description)) +{ +} + +ProcessActor::~ProcessActor() = default; + +void ProcessActor::handle_message(StringView type, JsonObject const&) +{ + send_unrecognized_packet_type_error(type); +} + +JsonObject ProcessActor::serialize_description() const +{ + JsonObject traits; + traits.set("watcher"sv, m_description.is_parent); + traits.set("supportsReloadDescriptor"sv, true); + + JsonObject description; + description.set("actor"sv, name()); + description.set("id"sv, m_description.id); + description.set("isParent"sv, m_description.is_parent); + description.set("isWindowlessParent"sv, m_description.is_windowless_parent); + description.set("traits"sv, move(traits)); + return description; +} + +} diff --git a/Libraries/LibDevTools/Actors/ProcessActor.h b/Libraries/LibDevTools/Actors/ProcessActor.h new file mode 100644 index 00000000000..15d7fba82b1 --- /dev/null +++ b/Libraries/LibDevTools/Actors/ProcessActor.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace DevTools { + +struct ProcessDescription { + u64 id { 0 }; + bool is_parent { false }; + bool is_windowless_parent { false }; +}; + +class ProcessActor final : public Actor { +public: + static constexpr auto base_name = "process"sv; + + static NonnullRefPtr create(DevToolsServer&, ByteString name, ProcessDescription); + virtual ~ProcessActor() override; + + virtual void handle_message(StringView type, JsonObject const&) override; + + ProcessDescription const& description() const { return m_description; } + JsonObject serialize_description() const; + +private: + ProcessActor(DevToolsServer&, ByteString name, ProcessDescription); + + ProcessDescription m_description; +}; + +} diff --git a/Libraries/LibDevTools/Actors/RootActor.cpp b/Libraries/LibDevTools/Actors/RootActor.cpp new file mode 100644 index 00000000000..8a8f4ecdcd9 --- /dev/null +++ b/Libraries/LibDevTools/Actors/RootActor.cpp @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include + +namespace DevTools { + +// https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#the-root-actor +NonnullRefPtr RootActor::create(DevToolsServer& devtools, ByteString name) +{ + auto actor = adopt_ref(*new RootActor(devtools, move(name))); + + JsonObject traits; + traits.set("sources"sv, false); + traits.set("highlightable"sv, true); + traits.set("customHighlighters"sv, true); + traits.set("networkMonitor"sv, false); + + JsonObject message; + message.set("from"sv, actor->name()); + message.set("applicationType"sv, "browser"sv); + message.set("traits"sv, move(traits)); + actor->send_message(move(message)); + + return actor; +} + +RootActor::RootActor(DevToolsServer& devtools, ByteString name) + : Actor(devtools, move(name)) +{ +} + +RootActor::~RootActor() = default; + +void RootActor::handle_message(StringView type, JsonObject const& message) +{ + JsonObject response; + response.set("from"sv, name()); + + if (type == "connect") { + send_message(move(response)); + return; + } + + if (type == "getRoot"sv) { + response.set("selected"sv, 0); + + for (auto const& actor : devtools().actor_registry()) { + if (is(*actor.value)) + response.set("deviceActor"sv, actor.key); + else if (is(*actor.value)) + response.set("preferenceActor"sv, actor.key); + } + + send_message(move(response)); + return; + } + + if (type == "getProcess"sv) { + auto id = message.get_integer("id"sv); + if (!id.has_value()) { + send_missing_parameter_error("id"sv); + return; + } + + for (auto const& actor : devtools().actor_registry()) { + auto const* process_actor = as_if(*actor.value); + if (!process_actor) + continue; + if (process_actor->description().id != *id) + continue; + + response.set("processDescriptor"sv, process_actor->serialize_description()); + break; + } + + send_message(move(response)); + return; + } + + if (type == "getTab"sv) { + response.set("tab"sv, JsonObject {}); + send_message(move(response)); + return; + } + + if (type == "listAddons"sv) { + response.set("addons"sv, JsonArray {}); + send_message(move(response)); + return; + } + + if (type == "listProcesses"sv) { + JsonArray processes; + + for (auto const& actor : devtools().actor_registry()) { + if (auto const* process_actor = as_if(*actor.value)) + processes.must_append(process_actor->serialize_description()); + } + + response.set("processes"sv, move(processes)); + send_message(move(response)); + return; + } + + if (type == "listServiceWorkerRegistrations"sv) { + response.set("registrations"sv, JsonArray {}); + send_message(move(response)); + return; + } + + if (type == "listTabs"sv) { + response.set("tabs"sv, JsonArray {}); + send_message(move(response)); + return; + } + + if (type == "listWorkers"sv) { + response.set("workers"sv, JsonArray {}); + send_message(move(response)); + return; + } + + send_unrecognized_packet_type_error(type); +} + +} diff --git a/Libraries/LibDevTools/Actors/RootActor.h b/Libraries/LibDevTools/Actors/RootActor.h new file mode 100644 index 00000000000..5ffa5c534b9 --- /dev/null +++ b/Libraries/LibDevTools/Actors/RootActor.h @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace DevTools { + +class RootActor final : public Actor { +public: + static constexpr auto base_name = "root"sv; + + static NonnullRefPtr create(DevToolsServer&, ByteString name); + virtual ~RootActor() override; + + virtual void handle_message(StringView type, JsonObject const&) override; + +private: + RootActor(DevToolsServer&, ByteString name); +}; + +} diff --git a/Libraries/LibDevTools/CMakeLists.txt b/Libraries/LibDevTools/CMakeLists.txt new file mode 100644 index 00000000000..646a8e3b179 --- /dev/null +++ b/Libraries/LibDevTools/CMakeLists.txt @@ -0,0 +1,12 @@ +set(SOURCES + Actor.cpp + Actors/DeviceActor.cpp + Actors/PreferenceActor.cpp + Actors/ProcessActor.cpp + Actors/RootActor.cpp + Connection.cpp + DevToolsServer.cpp +) + +serenity_lib(LibDevTools devtools) +target_link_libraries(LibDevTools PRIVATE LibCore LibWeb) diff --git a/Libraries/LibDevTools/Connection.cpp b/Libraries/LibDevTools/Connection.cpp new file mode 100644 index 00000000000..e34646f9315 --- /dev/null +++ b/Libraries/LibDevTools/Connection.cpp @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include + +namespace DevTools { + +NonnullRefPtr Connection::create(NonnullOwnPtr socket) +{ + return adopt_ref(*new Connection(move(socket))); +} + +Connection::Connection(NonnullOwnPtr socket) + : m_socket(move(socket)) +{ + m_socket->on_ready_to_read = [this]() { + if (auto result = on_ready_to_read(); result.is_error()) { + if (on_connection_closed) + on_connection_closed(); + } + }; +} + +Connection::~Connection() = default; + +// https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#packets +void Connection::send_message(JsonValue const& message) +{ + auto serialized = message.serialized(); + + if constexpr (DEVTOOLS_DEBUG) { + if (message.is_object() && message.as_object().get("error"sv).has_value()) + dbgln("\x1b[1;31m<<\x1b[0m {}", serialized); + else + dbgln("\x1b[1;32m<<\x1b[0m {}", serialized); + } + + if (m_socket->write_formatted("{}:{}", serialized.length(), serialized).is_error()) { + if (on_connection_closed) + on_connection_closed(); + } +} + +// https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#packets +ErrorOr Connection::read_message() +{ + ByteBuffer length_buffer; + + // FIXME: `read_until(':')` would be nicer here, but that seems to return immediately without receiving any data. + while (true) { + auto byte = TRY(m_socket->read_value()); + if (byte == ':') { + break; + } + + length_buffer.append(byte); + } + + auto length = StringView { length_buffer }.to_number(); + if (!length.has_value()) + return Error::from_string_literal("Could not read message length from DevTools client"); + + ByteBuffer message_buffer; + message_buffer.resize(*length); + + TRY(m_socket->read_until_filled(message_buffer)); + + auto message = TRY(JsonValue::from_string(message_buffer)); + dbgln_if(DEVTOOLS_DEBUG, "\x1b[1;33m>>\x1b[0m {}", message); + + return message; +} + +ErrorOr Connection::on_ready_to_read() +{ + // https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#the-request-reply-pattern + // Note that it is correct for a client to send several requests to a request/reply actor without waiting for a + // reply to each request before sending the next; requests can be pipelined. + while (TRY(m_socket->can_read_without_blocking())) { + auto message = TRY(read_message()); + if (!message.is_object()) + continue; + + Core::deferred_invoke([this, message = move(message)]() { + if (on_message_received) + on_message_received(message.as_object()); + }); + } + + return {}; +} + +} diff --git a/Libraries/LibDevTools/Connection.h b/Libraries/LibDevTools/Connection.h new file mode 100644 index 00000000000..9fe7490ee7c --- /dev/null +++ b/Libraries/LibDevTools/Connection.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace DevTools { + +class Connection : public RefCounted { +public: + static NonnullRefPtr create(NonnullOwnPtr); + ~Connection(); + + Function on_connection_closed; + Function on_message_received; + + void send_message(JsonValue const&); + +private: + explicit Connection(NonnullOwnPtr); + + ErrorOr on_ready_to_read(); + ErrorOr read_message(); + + NonnullOwnPtr m_socket; +}; + +} diff --git a/Libraries/LibDevTools/DevToolsDelegate.h b/Libraries/LibDevTools/DevToolsDelegate.h new file mode 100644 index 00000000000..4f0d36dedb6 --- /dev/null +++ b/Libraries/LibDevTools/DevToolsDelegate.h @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace DevTools { + +class DevToolsDelegate { +public: + virtual ~DevToolsDelegate() = default; +}; + +} diff --git a/Libraries/LibDevTools/DevToolsServer.cpp b/Libraries/LibDevTools/DevToolsServer.cpp new file mode 100644 index 00000000000..5cdc6a642f9 --- /dev/null +++ b/Libraries/LibDevTools/DevToolsServer.cpp @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace DevTools { + +static u64 s_server_count = 0; + +ErrorOr> DevToolsServer::create(DevToolsDelegate& delegate, u16 port) +{ + auto address = IPv4Address::from_string("0.0.0.0"sv).release_value(); + + auto server = TRY(Core::TCPServer::try_create()); + TRY(server->listen(address, port, Core::TCPServer::AllowAddressReuse::Yes)); + + return adopt_own(*new DevToolsServer(delegate, move(server))); +} + +DevToolsServer::DevToolsServer(DevToolsDelegate& delegate, NonnullRefPtr server) + : m_server(move(server)) + , m_delegate(delegate) + , m_server_id(s_server_count++) +{ + m_server->on_ready_to_accept = [this]() { + if (auto result = on_new_client(); result.is_error()) + warnln("Failed to accept DevTools client: {}", result.error()); + }; +} + +DevToolsServer::~DevToolsServer() = default; + +ErrorOr DevToolsServer::on_new_client() +{ + if (m_connection) + return Error::from_string_literal("Only one active DevTools connection is currently allowed"); + + auto client = TRY(m_server->accept()); + auto buffered_socket = TRY(Core::BufferedTCPSocket::create(move(client))); + + m_connection = Connection::create(move(buffered_socket)); + + m_connection->on_connection_closed = [this]() { + close_connection(); + }; + + m_connection->on_message_received = [this](auto const& message) { + on_message_received(message); + }; + + m_root_actor = register_actor(); + + register_actor(); + register_actor(); + register_actor(ProcessDescription { .is_parent = true }); + + return {}; +} + +void DevToolsServer::on_message_received(JsonObject const& message) +{ + auto to = message.get_byte_string("to"sv); + if (!to.has_value()) { + m_root_actor->send_missing_parameter_error("to"sv); + return; + } + + auto actor = m_actor_registry.find(*to); + if (actor == m_actor_registry.end()) { + m_root_actor->send_unknown_actor_error(*to); + return; + } + + auto type = message.get_byte_string("type"sv); + if (!type.has_value()) { + actor->value->send_missing_parameter_error("type"sv); + return; + } + + actor->value->handle_message(*type, message); +} + +void DevToolsServer::close_connection() +{ + dbgln_if(DEVTOOLS_DEBUG, "Lost connection to the DevTools client"); + + Core::deferred_invoke([this]() { + m_connection = nullptr; + m_actor_registry.clear(); + m_root_actor = nullptr; + }); +} + +} diff --git a/Libraries/LibDevTools/DevToolsServer.h b/Libraries/LibDevTools/DevToolsServer.h new file mode 100644 index 00000000000..3634c53769b --- /dev/null +++ b/Libraries/LibDevTools/DevToolsServer.h @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace DevTools { + +using ActorRegistry = HashMap>; + +class DevToolsServer { +public: + static ErrorOr> create(DevToolsDelegate&, u16 port); + ~DevToolsServer(); + + RefPtr& connection() { return m_connection; } + DevToolsDelegate const& delegate() const { return m_delegate; } + ActorRegistry const& actor_registry() const { return m_actor_registry; } + + template + ActorType& register_actor(Args&&... args) + { + ByteString name; + + if constexpr (IsSame) { + name = ActorType::base_name; + } else { + name = ByteString::formatted("server{}-{}{}", m_server_id, ActorType::base_name, m_actor_count); + } + + auto actor = ActorType::create(*this, name, forward(args)...); + m_actor_registry.set(name, actor); + ++m_actor_count; + + return actor; + } + +private: + explicit DevToolsServer(DevToolsDelegate&, NonnullRefPtr); + + ErrorOr on_new_client(); + void on_message_received(JsonObject const&); + + void close_connection(); + + NonnullRefPtr m_server; + RefPtr m_connection; + + DevToolsDelegate& m_delegate; + + ActorRegistry m_actor_registry; + RefPtr m_root_actor { nullptr }; + + u64 m_server_id { 0 }; + u64 m_actor_count { 0 }; +}; + +} diff --git a/Libraries/LibDevTools/Forward.h b/Libraries/LibDevTools/Forward.h new file mode 100644 index 00000000000..bcd39935cdf --- /dev/null +++ b/Libraries/LibDevTools/Forward.h @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +namespace DevTools { + +class Actor; +class Connection; +class DeviceActor; +class DevToolsDelegate; +class DevToolsServer; +class PreferenceActor; +class ProcessActor; +class RootActor; + +struct ProcessDescription; + +} diff --git a/Meta/CMake/all_the_debug_macros.cmake b/Meta/CMake/all_the_debug_macros.cmake index 79368e293c9..6fccf30c0c8 100644 --- a/Meta/CMake/all_the_debug_macros.cmake +++ b/Meta/CMake/all_the_debug_macros.cmake @@ -8,6 +8,7 @@ set(CSS_LOADER_DEBUG ON) set(CSS_PARSER_DEBUG ON) set(CSS_TOKENIZER_DEBUG ON) set(CSS_TRANSITIONS_DEBUG ON) +set(DEVTOOLS_DEBUG ON) set(DNS_DEBUG ON) set(EDITOR_DEBUG ON) set(EMOJI_DEBUG ON) diff --git a/Meta/Lagom/CMakeLists.txt b/Meta/Lagom/CMakeLists.txt index 216b1daf853..61834856034 100644 --- a/Meta/Lagom/CMakeLists.txt +++ b/Meta/Lagom/CMakeLists.txt @@ -413,6 +413,7 @@ set(lagom_standard_libraries if (ENABLE_GUI_TARGETS) list(APPEND lagom_standard_libraries + DevTools Gfx ImageDecoderClient Media