DevTools server callbacks can outlive the server when deferred work is queued during connection shutdown. Capture weak pointers for those tasks and clear socket callbacks while the server is being destroyed so later actor cleanup cannot touch freed state. Add protocol coverage that destroys a server with deferred actor cleanup pending, before adding the style-rule actor path that depends on this.
110 lines
3.4 KiB
C++
110 lines
3.4 KiB
C++
/*
|
|
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <AK/Debug.h>
|
|
#include <AK/JsonObject.h>
|
|
#include <AK/JsonValue.h>
|
|
#include <LibCore/EventLoop.h>
|
|
#include <LibDevTools/Connection.h>
|
|
|
|
namespace DevTools {
|
|
|
|
NonnullRefPtr<Connection> Connection::create(NonnullOwnPtr<Core::BufferedTCPSocket> socket)
|
|
{
|
|
return adopt_ref(*new Connection(move(socket)));
|
|
}
|
|
|
|
Connection::Connection(NonnullOwnPtr<Core::BufferedTCPSocket> 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);
|
|
}
|
|
|
|
// Temporarily enable blocking mode for large writes to avoid EAGAIN
|
|
(void)m_socket->set_blocking(true);
|
|
auto result = m_socket->write_until_depleted(MUST(String::formatted("{}:{}", serialized.byte_count(), serialized)));
|
|
(void)m_socket->set_blocking(false);
|
|
|
|
if (result.is_error()) {
|
|
warnln("DevTools: Failed to send message ({} bytes): {}", serialized.byte_count(), result.error());
|
|
if (on_connection_closed)
|
|
on_connection_closed();
|
|
}
|
|
}
|
|
|
|
// https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#packets
|
|
ErrorOr<JsonValue> 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<u8>());
|
|
if (byte == ':') {
|
|
break;
|
|
}
|
|
|
|
length_buffer.append(byte);
|
|
}
|
|
|
|
auto length = StringView { length_buffer }.to_number<size_t>();
|
|
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<void> 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([weak_self = make_weak_ptr<Connection>(), message = move(message)]() mutable {
|
|
auto self = weak_self.strong_ref();
|
|
if (!self)
|
|
return;
|
|
|
|
if (self->on_message_received)
|
|
self->on_message_received(move(message.as_object()));
|
|
});
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
}
|