LibDevTools: Implement enough of the protocol to inspect tabs

There is a lot needed all at once to actually inspect a tab's DOM tree.
It begins with requesting a "watcher" from a TabActor. It seems there
can be many types of watchers, but here we implement the "frame" watcher
only. The watcher creates an "inspector", which in turn creates a
"walker", which is the actor ultimately responsible for serializing and
inspecting the DOM tree.

In between all that, the DevTools client will send a handful of other
informational requests. If we do not reply to these, the client will not
move forward with the walker. For example, the CSSPropertiesActor will
be asked for a list of all known CSS properties.
This commit is contained in:
Timothy Flynn 2025-02-15 07:57:36 -05:00 committed by Tim Flynn
commit 5ed91dc915
Notes: github-actions[bot] 2025-02-19 13:47:12 +00:00
25 changed files with 1278 additions and 0 deletions

View file

@ -0,0 +1,58 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/JsonArray.h>
#include <AK/JsonObject.h>
#include <LibDevTools/Actors/CSSPropertiesActor.h>
#include <LibDevTools/DevToolsDelegate.h>
#include <LibDevTools/DevToolsServer.h>
namespace DevTools {
NonnullRefPtr<CSSPropertiesActor> CSSPropertiesActor::create(DevToolsServer& devtools, ByteString name)
{
return adopt_ref(*new CSSPropertiesActor(devtools, move(name)));
}
CSSPropertiesActor::CSSPropertiesActor(DevToolsServer& devtools, ByteString name)
: Actor(devtools, move(name))
{
}
CSSPropertiesActor::~CSSPropertiesActor() = default;
void CSSPropertiesActor::handle_message(StringView type, JsonObject const&)
{
JsonObject response;
response.set("from"sv, name());
if (type == "getCSSDatabase"sv) {
auto css_property_list = devtools().delegate().css_property_list();
JsonObject properties;
for (auto const& css_property : css_property_list) {
JsonArray subproperties;
subproperties.must_append(css_property.name);
JsonObject property;
property.set("isInherited"sv, css_property.is_inherited);
property.set("supports"sv, JsonArray {});
property.set("values"sv, JsonArray {});
property.set("subproperties"sv, move(subproperties));
properties.set(css_property.name, move(property));
}
response.set("properties"sv, move(properties));
send_message(move(response));
return;
}
send_unrecognized_packet_type_error(type);
}
}

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/ByteString.h>
#include <AK/NonnullRefPtr.h>
#include <LibDevTools/Actor.h>
namespace DevTools {
struct CSSProperty {
ByteString name;
bool is_inherited { false };
};
class CSSPropertiesActor final : public Actor {
public:
static constexpr auto base_name = "css-properties"sv;
static NonnullRefPtr<CSSPropertiesActor> create(DevToolsServer&, ByteString name);
virtual ~CSSPropertiesActor() override;
virtual void handle_message(StringView type, JsonObject const&) override;
private:
CSSPropertiesActor(DevToolsServer&, ByteString name);
};
}

View file

@ -0,0 +1,98 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/JsonArray.h>
#include <AK/JsonObject.h>
#include <LibDevTools/Actors/CSSPropertiesActor.h>
#include <LibDevTools/Actors/FrameActor.h>
#include <LibDevTools/Actors/InspectorActor.h>
#include <LibDevTools/Actors/TabActor.h>
#include <LibDevTools/Actors/ThreadActor.h>
namespace DevTools {
NonnullRefPtr<FrameActor> FrameActor::create(DevToolsServer& devtools, ByteString name, WeakPtr<TabActor> tab, WeakPtr<CSSPropertiesActor> css_properties, WeakPtr<InspectorActor> inspector, WeakPtr<ThreadActor> thread)
{
return adopt_ref(*new FrameActor(devtools, move(name), move(tab), move(css_properties), move(inspector), move(thread)));
}
FrameActor::FrameActor(DevToolsServer& devtools, ByteString name, WeakPtr<TabActor> tab, WeakPtr<CSSPropertiesActor> css_properties, WeakPtr<InspectorActor> inspector, WeakPtr<ThreadActor> thread)
: Actor(devtools, move(name))
, m_tab(move(tab))
, m_css_properties(move(css_properties))
, m_inspector(move(inspector))
, m_thread(move(thread))
{
}
FrameActor::~FrameActor() = default;
void FrameActor::handle_message(StringView type, JsonObject const&)
{
JsonObject response;
response.set("from"sv, name());
if (type == "listFrames"sv) {
send_message(move(response));
return;
}
send_unrecognized_packet_type_error(type);
}
void FrameActor::send_frame_update_message()
{
JsonArray frames;
if (auto tab_actor = m_tab.strong_ref()) {
JsonObject frame;
frame.set("id"sv, tab_actor->description().id);
frame.set("title"sv, tab_actor->description().title);
frame.set("url"sv, tab_actor->description().url);
frames.must_append(move(frame));
}
JsonObject message;
message.set("from", name());
message.set("type", "frameUpdate"sv);
message.set("frames", move(frames));
send_message(move(message));
}
JsonObject FrameActor::serialize_target() const
{
JsonObject traits;
traits.set("frames"sv, true);
traits.set("isBrowsingContext"sv, true);
traits.set("logInPage"sv, false);
traits.set("navigation"sv, true);
traits.set("supportsTopLevelTargetFlag"sv, true);
traits.set("watchpoints"sv, true);
JsonObject target;
target.set("actor"sv, name());
if (auto tab_actor = m_tab.strong_ref()) {
target.set("title"sv, tab_actor->description().title);
target.set("url"sv, tab_actor->description().url);
target.set("browsingContextID"sv, tab_actor->description().id);
target.set("outerWindowID"sv, tab_actor->description().id);
target.set("isTopLevelTarget"sv, true);
}
target.set("traits"sv, move(traits));
if (auto css_properties = m_css_properties.strong_ref())
target.set("cssPropertiesActor"sv, css_properties->name());
if (auto inspector = m_inspector.strong_ref())
target.set("inspectorActor"sv, inspector->name());
if (auto thread = m_thread.strong_ref())
target.set("threadActor"sv, thread->name());
return target;
}
}

View file

@ -0,0 +1,36 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/NonnullRefPtr.h>
#include <LibDevTools/Actor.h>
namespace DevTools {
class FrameActor final : public Actor {
public:
static constexpr auto base_name = "frame"sv;
static NonnullRefPtr<FrameActor> create(DevToolsServer&, ByteString name, WeakPtr<TabActor>, WeakPtr<CSSPropertiesActor>, WeakPtr<InspectorActor>, WeakPtr<ThreadActor>);
virtual ~FrameActor() override;
virtual void handle_message(StringView type, JsonObject const&) override;
void send_frame_update_message();
JsonObject serialize_target() const;
private:
FrameActor(DevToolsServer&, ByteString name, WeakPtr<TabActor>, WeakPtr<CSSPropertiesActor>, WeakPtr<InspectorActor>, WeakPtr<ThreadActor>);
WeakPtr<TabActor> m_tab;
WeakPtr<CSSPropertiesActor> m_css_properties;
WeakPtr<InspectorActor> m_inspector;
WeakPtr<ThreadActor> m_thread;
};
}

View file

@ -0,0 +1,50 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/JsonObject.h>
#include <LibDevTools/Actors/HighlighterActor.h>
namespace DevTools {
NonnullRefPtr<HighlighterActor> HighlighterActor::create(DevToolsServer& devtools, ByteString name)
{
return adopt_ref(*new HighlighterActor(devtools, move(name)));
}
HighlighterActor::HighlighterActor(DevToolsServer& devtools, ByteString name)
: Actor(devtools, move(name))
{
}
HighlighterActor::~HighlighterActor() = default;
void HighlighterActor::handle_message(StringView type, JsonObject const&)
{
JsonObject response;
response.set("from"sv, name());
if (type == "show"sv) {
response.set("value"sv, true);
send_message(move(response));
return;
}
if (type == "hide"sv) {
send_message(move(response));
return;
}
send_unrecognized_packet_type_error(type);
}
JsonValue HighlighterActor::serialize_highlighter() const
{
JsonObject highlighter;
highlighter.set("actor"sv, name());
return highlighter;
}
}

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/NonnullRefPtr.h>
#include <LibDevTools/Actor.h>
namespace DevTools {
class HighlighterActor final : public Actor {
public:
static constexpr auto base_name = "highlighter"sv;
static NonnullRefPtr<HighlighterActor> create(DevToolsServer&, ByteString name);
virtual ~HighlighterActor() override;
virtual void handle_message(StringView type, JsonObject const&) override;
JsonValue serialize_highlighter() const;
private:
HighlighterActor(DevToolsServer&, ByteString name);
};
}

View file

@ -0,0 +1,95 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Debug.h>
#include <AK/JsonObject.h>
#include <LibDevTools/Actors/HighlighterActor.h>
#include <LibDevTools/Actors/InspectorActor.h>
#include <LibDevTools/Actors/PageStyleActor.h>
#include <LibDevTools/Actors/TabActor.h>
#include <LibDevTools/Actors/WalkerActor.h>
#include <LibDevTools/DevToolsDelegate.h>
#include <LibDevTools/DevToolsServer.h>
namespace DevTools {
NonnullRefPtr<InspectorActor> InspectorActor::create(DevToolsServer& devtools, ByteString name, WeakPtr<TabActor> tab)
{
return adopt_ref(*new InspectorActor(devtools, move(name), move(tab)));
}
InspectorActor::InspectorActor(DevToolsServer& devtools, ByteString name, WeakPtr<TabActor> tab)
: Actor(devtools, move(name))
, m_tab(move(tab))
{
}
InspectorActor::~InspectorActor() = default;
void InspectorActor::handle_message(StringView type, JsonObject const&)
{
JsonObject response;
response.set("from"sv, name());
if (type == "getPageStyle"sv) {
if (!m_page_style)
m_page_style = devtools().register_actor<PageStyleActor>();
response.set("pageStyle"sv, m_page_style->serialize_style());
send_message(move(response));
return;
}
if (type == "getHighlighterByType"sv) {
if (!m_highlighter)
m_highlighter = devtools().register_actor<HighlighterActor>();
response.set("highlighter"sv, m_highlighter->serialize_highlighter());
send_message(move(response));
return;
}
if (type == "getWalker"sv) {
if (auto tab = m_tab.strong_ref()) {
auto block_token = block_responses();
devtools().delegate().inspect_tab(tab->description(),
[weak_self = make_weak_ptr<InspectorActor>(), block_token = move(block_token)](ErrorOr<JsonValue> dom_tree) mutable {
if (dom_tree.is_error()) {
dbgln_if(DEVTOOLS_DEBUG, "Unable to retrieve DOM tree: {}", dom_tree.error());
return;
}
if (!WalkerActor::is_suitable_for_dom_inspection(dom_tree.value())) {
dbgln_if(DEVTOOLS_DEBUG, "Did not receive a suitable DOM tree: {}", dom_tree);
return;
}
if (auto self = weak_self.strong_ref())
self->received_dom_tree(move(dom_tree.release_value().as_object()), move(block_token));
});
}
return;
}
send_unrecognized_packet_type_error(type);
}
void InspectorActor::received_dom_tree(JsonObject dom_tree, BlockToken block_token)
{
auto& walker_actor = devtools().register_actor<WalkerActor>(m_tab, move(dom_tree));
JsonObject walker;
walker.set("actor"sv, walker_actor.name());
walker.set("root"sv, walker_actor.serialize_root());
JsonObject message;
message.set("from"sv, name());
message.set("walker"sv, move(walker));
send_message(move(message), move(block_token));
}
}

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/NonnullRefPtr.h>
#include <LibDevTools/Actor.h>
namespace DevTools {
class InspectorActor final : public Actor {
public:
static constexpr auto base_name = "inspector"sv;
static NonnullRefPtr<InspectorActor> create(DevToolsServer&, ByteString name, WeakPtr<TabActor>);
virtual ~InspectorActor() override;
virtual void handle_message(StringView type, JsonObject const&) override;
private:
InspectorActor(DevToolsServer&, ByteString name, WeakPtr<TabActor>);
void received_dom_tree(JsonObject, BlockToken);
WeakPtr<TabActor> m_tab;
WeakPtr<PageStyleActor> m_page_style;
WeakPtr<HighlighterActor> m_highlighter;
};
}

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/JsonObject.h>
#include <LibDevTools/Actors/PageStyleActor.h>
namespace DevTools {
NonnullRefPtr<PageStyleActor> PageStyleActor::create(DevToolsServer& devtools, ByteString name)
{
return adopt_ref(*new PageStyleActor(devtools, move(name)));
}
PageStyleActor::PageStyleActor(DevToolsServer& devtools, ByteString name)
: Actor(devtools, move(name))
{
}
PageStyleActor::~PageStyleActor() = default;
void PageStyleActor::handle_message(StringView type, JsonObject const&)
{
send_unrecognized_packet_type_error(type);
}
JsonValue PageStyleActor::serialize_style() const
{
JsonObject traits;
traits.set("fontStyleLevel4"sv, true);
traits.set("fontWeightLevel4"sv, true);
traits.set("fontStretchLevel4"sv, true);
traits.set("fontVariations"sv, true);
JsonObject style;
style.set("actor"sv, name());
style.set("traits"sv, move(traits));
return style;
}
}

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/NonnullRefPtr.h>
#include <LibDevTools/Actor.h>
namespace DevTools {
class PageStyleActor final : public Actor {
public:
static constexpr auto base_name = "page-style"sv;
static NonnullRefPtr<PageStyleActor> create(DevToolsServer&, ByteString name);
virtual ~PageStyleActor() override;
virtual void handle_message(StringView type, JsonObject const&) override;
JsonValue serialize_style() const;
private:
PageStyleActor(DevToolsServer&, ByteString name);
};
}

View file

@ -6,6 +6,7 @@
#include <AK/JsonObject.h> #include <AK/JsonObject.h>
#include <LibDevTools/Actors/TabActor.h> #include <LibDevTools/Actors/TabActor.h>
#include <LibDevTools/Actors/WatcherActor.h>
#include <LibDevTools/DevToolsServer.h> #include <LibDevTools/DevToolsServer.h>
namespace DevTools { namespace DevTools {
@ -36,6 +37,16 @@ void TabActor::handle_message(StringView type, JsonObject const&)
return; return;
} }
if (type == "getWatcher"sv) {
if (!m_watcher)
m_watcher = devtools().register_actor<WatcherActor>(this);
response.set("actor"sv, m_watcher->name());
response.set("traits"sv, m_watcher->serialize_description());
send_message(move(response));
return;
}
send_unrecognized_packet_type_error(type); send_unrecognized_packet_type_error(type);
} }

View file

@ -33,6 +33,7 @@ private:
TabActor(DevToolsServer&, ByteString name, TabDescription); TabActor(DevToolsServer&, ByteString name, TabDescription);
TabDescription m_description; TabDescription m_description;
WeakPtr<WatcherActor> m_watcher;
}; };
} }

View file

@ -0,0 +1,76 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/JsonArray.h>
#include <AK/JsonObject.h>
#include <LibDevTools/Actors/TargetConfigurationActor.h>
namespace DevTools {
NonnullRefPtr<TargetConfigurationActor> TargetConfigurationActor::create(DevToolsServer& devtools, ByteString name)
{
return adopt_ref(*new TargetConfigurationActor(devtools, move(name)));
}
TargetConfigurationActor::TargetConfigurationActor(DevToolsServer& devtools, ByteString name)
: Actor(devtools, move(name))
{
}
TargetConfigurationActor::~TargetConfigurationActor() = default;
void TargetConfigurationActor::handle_message(StringView type, JsonObject const& message)
{
JsonObject response;
response.set("from"sv, name());
if (type == "updateConfiguration"sv) {
auto configuration = message.get_object("configuration"sv);
if (!configuration.has_value()) {
send_missing_parameter_error("configuration"sv);
return;
}
send_message(move(response));
return;
}
send_unrecognized_packet_type_error(type);
}
JsonObject TargetConfigurationActor::serialize_configuration() const
{
JsonObject supported_options;
supported_options.set("cacheDisabled"sv, false);
supported_options.set("colorSchemeSimulation"sv, false);
supported_options.set("customFormatters"sv, false);
supported_options.set("customUserAgent"sv, false);
supported_options.set("javascriptEnabled"sv, false);
supported_options.set("overrideDPPX"sv, false);
supported_options.set("printSimulationEnabled"sv, false);
supported_options.set("rdmPaneMaxTouchPoints"sv, false);
supported_options.set("rdmPaneOrientation"sv, false);
supported_options.set("recordAllocations"sv, false);
supported_options.set("reloadOnTouchSimulationToggle"sv, false);
supported_options.set("restoreFocus"sv, false);
supported_options.set("serviceWorkersTestingEnabled"sv, false);
supported_options.set("setTabOffline"sv, false);
supported_options.set("touchEventsOverride"sv, false);
supported_options.set("tracerOptions"sv, false);
supported_options.set("useSimpleHighlightersForReducedMotion"sv, false);
JsonObject traits;
traits.set("supportedOptions"sv, move(supported_options));
JsonObject target;
target.set("actor"sv, name());
target.set("configuration"sv, JsonObject {});
target.set("traits"sv, move(traits));
return target;
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/NonnullRefPtr.h>
#include <LibDevTools/Actor.h>
namespace DevTools {
class TargetConfigurationActor final : public Actor {
public:
static constexpr auto base_name = "target-configuration"sv;
static NonnullRefPtr<TargetConfigurationActor> create(DevToolsServer&, ByteString name);
virtual ~TargetConfigurationActor() override;
virtual void handle_message(StringView type, JsonObject const&) override;
JsonObject serialize_configuration() const;
private:
TargetConfigurationActor(DevToolsServer&, ByteString name);
};
}

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibDevTools/Actors/ThreadActor.h>
namespace DevTools {
NonnullRefPtr<ThreadActor> ThreadActor::create(DevToolsServer& devtools, ByteString name)
{
return adopt_ref(*new ThreadActor(devtools, move(name)));
}
ThreadActor::ThreadActor(DevToolsServer& devtools, ByteString name)
: Actor(devtools, move(name))
{
}
ThreadActor::~ThreadActor() = default;
void ThreadActor::handle_message(StringView type, JsonObject const&)
{
send_unrecognized_packet_type_error(type);
}
}

View file

@ -0,0 +1,27 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/NonnullRefPtr.h>
#include <LibDevTools/Actor.h>
namespace DevTools {
class ThreadActor final : public Actor {
public:
static constexpr auto base_name = "thread"sv;
static NonnullRefPtr<ThreadActor> create(DevToolsServer&, ByteString name);
virtual ~ThreadActor() override;
virtual void handle_message(StringView type, JsonObject const&) override;
private:
ThreadActor(DevToolsServer&, ByteString name);
};
}

View file

@ -0,0 +1,52 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/JsonArray.h>
#include <AK/JsonObject.h>
#include <LibDevTools/Actors/ThreadConfigurationActor.h>
namespace DevTools {
NonnullRefPtr<ThreadConfigurationActor> ThreadConfigurationActor::create(DevToolsServer& devtools, ByteString name)
{
return adopt_ref(*new ThreadConfigurationActor(devtools, move(name)));
}
ThreadConfigurationActor::ThreadConfigurationActor(DevToolsServer& devtools, ByteString name)
: Actor(devtools, move(name))
{
}
ThreadConfigurationActor::~ThreadConfigurationActor() = default;
void ThreadConfigurationActor::handle_message(StringView type, JsonObject const& message)
{
JsonObject response;
response.set("from"sv, name());
if (type == "updateConfiguration"sv) {
auto configuration = message.get_object("configuration"sv);
if (!configuration.has_value()) {
send_missing_parameter_error("configuration"sv);
return;
}
send_message(move(response));
return;
}
send_unrecognized_packet_type_error(type);
}
JsonObject ThreadConfigurationActor::serialize_configuration() const
{
JsonObject target;
target.set("actor"sv, name());
return target;
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/NonnullRefPtr.h>
#include <LibDevTools/Actor.h>
namespace DevTools {
class ThreadConfigurationActor final : public Actor {
public:
static constexpr auto base_name = "thread-configuration"sv;
static NonnullRefPtr<ThreadConfigurationActor> create(DevToolsServer&, ByteString name);
virtual ~ThreadConfigurationActor() override;
virtual void handle_message(StringView type, JsonObject const&) override;
JsonObject serialize_configuration() const;
private:
ThreadConfigurationActor(DevToolsServer&, ByteString name);
};
}

View file

@ -0,0 +1,275 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/JsonArray.h>
#include <LibDevTools/Actors/TabActor.h>
#include <LibDevTools/Actors/WalkerActor.h>
#include <LibWeb/DOM/NodeType.h>
namespace DevTools {
NonnullRefPtr<WalkerActor> WalkerActor::create(DevToolsServer& devtools, ByteString name, WeakPtr<TabActor> tab, JsonObject dom_tree)
{
return adopt_ref(*new WalkerActor(devtools, move(name), move(tab), move(dom_tree)));
}
WalkerActor::WalkerActor(DevToolsServer& devtools, ByteString name, WeakPtr<TabActor> tab, JsonObject dom_tree)
: Actor(devtools, move(name))
, m_tab(move(tab))
, m_dom_tree(move(dom_tree))
{
populate_dom_tree_cache(m_dom_tree);
}
WalkerActor::~WalkerActor() = default;
void WalkerActor::handle_message(StringView type, JsonObject const& message)
{
JsonObject response;
response.set("from"sv, name());
if (type == "children"sv) {
auto node = message.get_byte_string("node"sv);
if (!node.has_value()) {
send_missing_parameter_error("node"sv);
return;
}
JsonArray nodes;
if (auto ancestor_node = m_actor_to_dom_node_map.get(*node); ancestor_node.has_value()) {
if (auto children = ancestor_node.value()->get_array("children"sv); children.has_value()) {
children->for_each([&](JsonValue const& child) {
nodes.must_append(serialize_node(child.as_object()));
});
}
}
response.set("hasFirst"sv, !nodes.is_empty());
response.set("hasLast"sv, !nodes.is_empty());
response.set("nodes"sv, move(nodes));
send_message(move(response));
return;
}
if (type == "querySelector"sv) {
auto node = message.get_byte_string("node"sv);
if (!node.has_value()) {
send_missing_parameter_error("node"sv);
return;
}
auto selector = message.get_byte_string("selector"sv);
if (!selector.has_value()) {
send_missing_parameter_error("selector"sv);
return;
}
if (auto ancestor_node = m_actor_to_dom_node_map.get(*node); ancestor_node.has_value()) {
if (auto selected_node = find_node_by_selector(*ancestor_node.value(), *selector); selected_node.has_value()) {
response.set("node"sv, serialize_node(*selected_node));
if (auto parent = m_dom_node_to_parent_map.get(&selected_node.value()); parent.value() && parent.value() != ancestor_node.value()) {
// FIXME: Should this be a stack of nodes leading to `ancestor_node`?
JsonArray new_parents;
new_parents.must_append(serialize_node(*parent.value()));
response.set("newParents"sv, move(new_parents));
}
}
}
send_message(move(response));
return;
}
if (type == "watchRootNode"sv) {
response.set("type"sv, "root-available"sv);
response.set("node"sv, serialize_root());
send_message(move(response));
JsonObject message;
message.set("from", name());
send_message(move(message));
return;
}
send_unrecognized_packet_type_error(type);
}
bool WalkerActor::is_suitable_for_dom_inspection(JsonValue const& node)
{
if (!node.is_object())
return true;
auto const& object = node.as_object();
if (!object.has_string("name"sv) || !object.has_string("type"sv))
return false;
if (auto text = object.get_byte_string("text"sv); text.has_value()) {
if (text->is_whitespace())
return false;
}
if (auto data = object.get_byte_string("data"sv); data.has_value()) {
if (data->is_whitespace())
return false;
}
return true;
}
JsonValue WalkerActor::serialize_root() const
{
return serialize_node(m_dom_tree);
}
JsonValue WalkerActor::serialize_node(JsonObject const& node) const
{
auto tab = m_tab.strong_ref();
if (!tab)
return {};
auto actor = node.get_byte_string("actor"sv);
if (!actor.has_value())
return {};
auto name = node.get_byte_string("name"sv).release_value();
auto type = node.get_byte_string("type"sv).release_value();
auto dom_type = Web::DOM::NodeType::INVALID;
JsonValue node_value;
auto is_top_level_document = &node == &m_dom_tree;
auto is_displayed = !is_top_level_document && node.get_bool("visible"sv).value_or(false);
auto is_scrollable = node.get_bool("scrollable"sv).value_or(false);
auto is_shadow_root = false;
if (type == "document"sv) {
dom_type = Web::DOM::NodeType::DOCUMENT_NODE;
} else if (type == "element"sv) {
dom_type = Web::DOM::NodeType::ELEMENT_NODE;
} else if (type == "text"sv) {
dom_type = Web::DOM::NodeType::TEXT_NODE;
if (auto text = node.get_byte_string("text"sv); text.has_value())
node_value = text.release_value();
} else if (type == "comment"sv) {
dom_type = Web::DOM::NodeType::COMMENT_NODE;
if (auto data = node.get_byte_string("data"sv); data.has_value())
node_value = data.release_value();
} else if (type == "shadow-root"sv) {
is_shadow_root = true;
}
size_t child_count = 0;
if (auto children = node.get_array("children"sv); children.has_value())
child_count = children->size();
JsonArray attrs;
if (auto attributes = node.get_object("attributes"sv); attributes.has_value()) {
attributes->for_each_member([&](ByteString const& name, JsonValue const& value) {
if (!value.is_string())
return;
JsonObject attr;
attr.set("name"sv, name);
attr.set("value"sv, value.as_string());
attrs.must_append(move(attr));
});
}
JsonObject serialized;
serialized.set("actor"sv, actor.release_value());
serialized.set("attrs"sv, move(attrs));
serialized.set("baseURI"sv, tab->description().url);
serialized.set("causesOverflow"sv, false);
serialized.set("containerType"sv, JsonValue {});
serialized.set("displayName"sv, name.to_lowercase());
serialized.set("displayType"sv, "block");
serialized.set("host"sv, JsonValue {});
serialized.set("isAfterPseudoElement"sv, false);
serialized.set("isAnonymous"sv, false);
serialized.set("isBeforePseudoElement"sv, false);
serialized.set("isDirectShadowHostChild"sv, JsonValue {});
serialized.set("isDisplayed"sv, is_displayed);
serialized.set("isInHTMLDocument"sv, true);
serialized.set("isMarkerPseudoElement"sv, false);
serialized.set("isNativeAnonymous"sv, false);
serialized.set("isScrollable"sv, is_scrollable);
serialized.set("isShadowHost"sv, false);
serialized.set("isShadowRoot"sv, is_shadow_root);
serialized.set("isTopLevelDocument"sv, is_top_level_document);
serialized.set("nodeName"sv, name);
serialized.set("nodeType"sv, to_underlying(dom_type));
serialized.set("nodeValue"sv, move(node_value));
serialized.set("numChildren"sv, child_count);
serialized.set("shadowRootMode"sv, JsonValue {});
serialized.set("traits"sv, JsonObject {});
if (!is_top_level_document) {
if (auto parent = m_dom_node_to_parent_map.get(&node); parent.has_value() && parent.value()) {
actor = parent.value()->get_byte_string("actor"sv);
if (!actor.has_value())
return {};
serialized.set("parent"sv, actor.release_value());
}
}
return serialized;
}
Optional<JsonObject const&> WalkerActor::find_node_by_selector(JsonObject const& node, StringView selector)
{
auto matches = [&](auto const& candidate) {
return candidate.get_byte_string("name"sv)->equals_ignoring_ascii_case(selector);
};
if (matches(node))
return node;
if (auto children = node.get_array("children"sv); children.has_value()) {
for (size_t i = 0; i < children->size(); ++i) {
auto const& child = children->at(i);
if (matches(child.as_object()))
return child.as_object();
if (auto result = find_node_by_selector(child.as_object(), selector); result.has_value())
return result;
}
}
return {};
}
void WalkerActor::populate_dom_tree_cache(JsonObject& node, JsonObject const* parent)
{
m_dom_node_to_parent_map.set(&node, parent);
auto actor = ByteString::formatted("{}-node{}", name(), m_dom_node_count++);
m_actor_to_dom_node_map.set(actor, &node);
node.set("actor"sv, actor);
auto children = node.get_array("children"sv);
if (!children.has_value())
return;
children->values().remove_all_matching([&](JsonValue const& child) {
return !is_suitable_for_dom_inspection(child);
});
children->for_each([&](JsonValue& child) {
populate_dom_tree_cache(child.as_object(), &node);
});
}
}

View file

@ -0,0 +1,45 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/HashMap.h>
#include <AK/JsonObject.h>
#include <AK/NonnullRefPtr.h>
#include <AK/Optional.h>
#include <LibDevTools/Actor.h>
namespace DevTools {
class WalkerActor final : public Actor {
public:
static constexpr auto base_name = "walker"sv;
static NonnullRefPtr<WalkerActor> create(DevToolsServer&, ByteString name, WeakPtr<TabActor>, JsonObject dom_tree);
virtual ~WalkerActor() override;
virtual void handle_message(StringView type, JsonObject const&) override;
static bool is_suitable_for_dom_inspection(JsonValue const&);
JsonValue serialize_root() const;
private:
WalkerActor(DevToolsServer&, ByteString name, WeakPtr<TabActor>, JsonObject dom_tree);
JsonValue serialize_node(JsonObject const&) const;
Optional<JsonObject const&> find_node_by_selector(JsonObject const& node, StringView selector);
void populate_dom_tree_cache(JsonObject& node, JsonObject const* parent = nullptr);
WeakPtr<TabActor> m_tab;
JsonObject m_dom_tree;
HashMap<JsonObject const*, JsonObject const*> m_dom_node_to_parent_map;
HashMap<ByteString, JsonObject const*> m_actor_to_dom_node_map;
size_t m_dom_node_count { 0 };
};
}

View file

@ -0,0 +1,140 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/JsonObject.h>
#include <LibCore/EventLoop.h>
#include <LibDevTools/Actors/CSSPropertiesActor.h>
#include <LibDevTools/Actors/FrameActor.h>
#include <LibDevTools/Actors/InspectorActor.h>
#include <LibDevTools/Actors/TabActor.h>
#include <LibDevTools/Actors/TargetConfigurationActor.h>
#include <LibDevTools/Actors/ThreadActor.h>
#include <LibDevTools/Actors/ThreadConfigurationActor.h>
#include <LibDevTools/Actors/WatcherActor.h>
#include <LibDevTools/DevToolsServer.h>
namespace DevTools {
NonnullRefPtr<WatcherActor> WatcherActor::create(DevToolsServer& devtools, ByteString name, WeakPtr<TabActor> tab)
{
return adopt_ref(*new WatcherActor(devtools, move(name), move(tab)));
}
WatcherActor::WatcherActor(DevToolsServer& devtools, ByteString name, WeakPtr<TabActor> tab)
: Actor(devtools, move(name))
, m_tab(move(tab))
{
}
WatcherActor::~WatcherActor() = default;
void WatcherActor::handle_message(StringView type, JsonObject const& message)
{
JsonObject response;
response.set("from"sv, name());
if (type == "getParentBrowsingContextID"sv) {
auto browsing_context_id = message.get_integer<u64>("browsingContextID"sv);
if (!browsing_context_id.has_value()) {
send_missing_parameter_error("browsingContextID"sv);
return;
}
response.set("browsingContextID"sv, *browsing_context_id);
send_message(move(response));
return;
}
if (type == "getTargetConfigurationActor"sv) {
if (!m_target_configuration)
m_target_configuration = devtools().register_actor<TargetConfigurationActor>();
response.set("configuration"sv, m_target_configuration->serialize_configuration());
send_message(move(response));
return;
}
if (type == "getThreadConfigurationActor"sv) {
if (!m_thread_configuration)
m_thread_configuration = devtools().register_actor<ThreadConfigurationActor>();
response.set("configuration"sv, m_thread_configuration->serialize_configuration());
send_message(move(response));
return;
}
if (type == "watchTargets"sv) {
auto target_type = message.get_byte_string("targetType"sv);
if (!target_type.has_value()) {
send_missing_parameter_error("targetType"sv);
return;
}
if (target_type == "frame"sv) {
auto& css_properties = devtools().register_actor<CSSPropertiesActor>();
auto& inspector = devtools().register_actor<InspectorActor>(m_tab);
auto& thread = devtools().register_actor<ThreadActor>();
auto& target = devtools().register_actor<FrameActor>(m_tab, css_properties, inspector, thread);
m_target = target;
response.set("type"sv, "target-available-form"sv);
response.set("target"sv, target.serialize_target());
send_message(move(response));
target.send_frame_update_message();
JsonObject message;
message.set("from", name());
send_message(move(message));
return;
}
}
send_unrecognized_packet_type_error(type);
}
JsonObject WatcherActor::serialize_description() const
{
JsonObject resources;
resources.set("Cache"sv, false);
resources.set("console-message"sv, false);
resources.set("cookies"sv, false);
resources.set("css-change"sv, false);
resources.set("css-message"sv, false);
resources.set("css-registered-properties"sv, false);
resources.set("document-event"sv, false);
resources.set("error-message"sv, false);
resources.set("extension-storage"sv, false);
resources.set("indexed-db"sv, false);
resources.set("jstracer-state"sv, false);
resources.set("jstracer-trace"sv, false);
resources.set("last-private-context-exit"sv, false);
resources.set("local-storage"sv, false);
resources.set("network-event"sv, false);
resources.set("network-event-stacktrace"sv, false);
resources.set("platform-message"sv, false);
resources.set("reflow"sv, false);
resources.set("server-sent-event"sv, false);
resources.set("session-storage"sv, false);
resources.set("source"sv, false);
resources.set("stylesheet"sv, false);
resources.set("thread-state"sv, false);
resources.set("websocket"sv, false);
JsonObject description;
description.set("shared_worker"sv, false);
description.set("service_worker"sv, false);
description.set("frame"sv, true);
description.set("process"sv, false);
description.set("worker"sv, false);
description.set("resources"sv, move(resources));
return description;
}
}

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/NonnullRefPtr.h>
#include <LibDevTools/Actor.h>
namespace DevTools {
class WatcherActor final : public Actor {
public:
static constexpr auto base_name = "watcher"sv;
static NonnullRefPtr<WatcherActor> create(DevToolsServer&, ByteString name, WeakPtr<TabActor>);
virtual ~WatcherActor() override;
virtual void handle_message(StringView type, JsonObject const&) override;
JsonObject serialize_description() const;
private:
WatcherActor(DevToolsServer&, ByteString name, WeakPtr<TabActor>);
WeakPtr<TabActor> m_tab;
WeakPtr<Actor> m_target;
WeakPtr<TargetConfigurationActor> m_target_configuration;
WeakPtr<ThreadConfigurationActor> m_thread_configuration;
};
}

View file

@ -1,10 +1,20 @@
set(SOURCES set(SOURCES
Actor.cpp Actor.cpp
Actors/CSSPropertiesActor.cpp
Actors/DeviceActor.cpp Actors/DeviceActor.cpp
Actors/FrameActor.cpp
Actors/HighlighterActor.cpp
Actors/InspectorActor.cpp
Actors/PageStyleActor.cpp
Actors/PreferenceActor.cpp Actors/PreferenceActor.cpp
Actors/ProcessActor.cpp Actors/ProcessActor.cpp
Actors/RootActor.cpp Actors/RootActor.cpp
Actors/TabActor.cpp Actors/TabActor.cpp
Actors/TargetConfigurationActor.cpp
Actors/ThreadActor.cpp
Actors/ThreadConfigurationActor.cpp
Actors/WalkerActor.cpp
Actors/WatcherActor.cpp
Connection.cpp Connection.cpp
DevToolsServer.cpp DevToolsServer.cpp
) )

View file

@ -6,7 +6,11 @@
#pragma once #pragma once
#include <AK/Error.h>
#include <AK/Function.h>
#include <AK/JsonValue.h>
#include <AK/Vector.h> #include <AK/Vector.h>
#include <LibDevTools/Actors/CSSPropertiesActor.h>
#include <LibDevTools/Actors/TabActor.h> #include <LibDevTools/Actors/TabActor.h>
#include <LibDevTools/Forward.h> #include <LibDevTools/Forward.h>
@ -17,6 +21,10 @@ public:
virtual ~DevToolsDelegate() = default; virtual ~DevToolsDelegate() = default;
virtual Vector<TabDescription> tab_list() const { return {}; } virtual Vector<TabDescription> tab_list() const { return {}; }
virtual Vector<CSSProperty> css_property_list() const { return {}; }
using OnTabInspectionComplete = Function<void(ErrorOr<JsonValue>)>;
virtual void inspect_tab(TabDescription const&, OnTabInspectionComplete) const { }
}; };
} }

View file

@ -10,14 +10,25 @@ namespace DevTools {
class Actor; class Actor;
class Connection; class Connection;
class CSSPropertiesActor;
class DeviceActor; class DeviceActor;
class DevToolsDelegate; class DevToolsDelegate;
class DevToolsServer; class DevToolsServer;
class FrameActor;
class HighlighterActor;
class InspectorActor;
class PageStyleActor;
class PreferenceActor; class PreferenceActor;
class ProcessActor; class ProcessActor;
class RootActor; class RootActor;
class TabActor; class TabActor;
class TargetConfigurationActor;
class ThreadActor;
class ThreadConfigurationActor;
class WalkerActor;
class WatcherActor;
struct CSSProperty;
struct ProcessDescription; struct ProcessDescription;
struct TabDescription; struct TabDescription;