Previously, console messages were sent using an index-based system where DevTools would be notified of new message indices and then request them in batches. This created synchronization issues during page navigation when the WebContent process resets while DevTools still has stale index state. This changes to a push-based model where console messages are sent immediately as resources when they are logged, matching how Firefox DevTools handles console messages. Each message is pushed through IPC and forwarded to DevTools as a "console-message" or "error-message" resource. This eliminates the need for index tracking in FrameActor and simplifies the entire console message pipeline from WebContent through to DevTools.
174 lines
5.9 KiB
C++
174 lines
5.9 KiB
C++
/*
|
|
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <AK/GenericShorthands.h>
|
|
#include <AK/JsonObject.h>
|
|
#include <AK/JsonValue.h>
|
|
#include <AK/MemoryStream.h>
|
|
#include <LibJS/Print.h>
|
|
#include <LibJS/Runtime/BigInt.h>
|
|
#include <LibJS/Runtime/Realm.h>
|
|
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
|
|
#include <LibWeb/HTML/Window.h>
|
|
#include <WebContent/ConsoleGlobalEnvironmentExtensions.h>
|
|
#include <WebContent/DevToolsConsoleClient.h>
|
|
#include <WebContent/PageClient.h>
|
|
|
|
namespace WebContent {
|
|
|
|
GC_DEFINE_ALLOCATOR(DevToolsConsoleClient);
|
|
|
|
GC::Ref<DevToolsConsoleClient> DevToolsConsoleClient::create(JS::Realm& realm, JS::Console& console, PageClient& client)
|
|
{
|
|
auto& window = as<Web::HTML::Window>(realm.global_object());
|
|
auto console_global_environment_extensions = realm.create<ConsoleGlobalEnvironmentExtensions>(realm, window);
|
|
|
|
return realm.heap().allocate<DevToolsConsoleClient>(realm, console, client, console_global_environment_extensions);
|
|
}
|
|
|
|
DevToolsConsoleClient::DevToolsConsoleClient(JS::Realm& realm, JS::Console& console, PageClient& client, ConsoleGlobalEnvironmentExtensions& console_global_environment_extensions)
|
|
: WebContentConsoleClient(realm, console, client, console_global_environment_extensions)
|
|
{
|
|
}
|
|
|
|
DevToolsConsoleClient::~DevToolsConsoleClient() = default;
|
|
|
|
// https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#grips
|
|
static JsonValue serialize_js_value(JS::Realm& realm, JS::Value value)
|
|
{
|
|
auto& vm = realm.vm();
|
|
|
|
auto serialize_type = [](StringView type) {
|
|
JsonObject serialized;
|
|
serialized.set("type"sv, type);
|
|
return serialized;
|
|
};
|
|
|
|
if (value.is_undefined())
|
|
return serialize_type("undefined"sv);
|
|
|
|
if (value.is_null())
|
|
return serialize_type("null"sv);
|
|
|
|
if (value.is_boolean())
|
|
return value.as_bool();
|
|
|
|
if (value.is_string())
|
|
return value.as_string().utf8_string();
|
|
|
|
if (value.is_number()) {
|
|
if (value.is_nan())
|
|
return serialize_type("NaN"sv);
|
|
if (value.is_positive_infinity())
|
|
return serialize_type("Infinity"sv);
|
|
if (value.is_negative_infinity())
|
|
return serialize_type("-Infinity"sv);
|
|
if (value.is_negative_zero())
|
|
return serialize_type("-0"sv);
|
|
return value.as_double();
|
|
}
|
|
|
|
if (value.is_bigint()) {
|
|
auto serialized = serialize_type("BigInt"sv);
|
|
serialized.set("text"sv, MUST(value.as_bigint().big_integer().to_base(10)));
|
|
return serialized;
|
|
}
|
|
|
|
if (value.is_symbol())
|
|
return value.as_symbol().descriptive_string().to_utf8();
|
|
|
|
// FIXME: Handle serialization of object grips. For now, we stringify the object.
|
|
if (value.is_object()) {
|
|
Web::HTML::TemporaryExecutionContext execution_context { realm };
|
|
AllocatingMemoryStream stream;
|
|
|
|
JS::PrintContext context { vm, stream, true };
|
|
MUST(JS::print(value, context));
|
|
|
|
return MUST(String::from_stream(stream, stream.used_buffer_size()));
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
void DevToolsConsoleClient::handle_result(JS::Value result)
|
|
{
|
|
m_client->did_execute_js_console_input(serialize_js_value(m_realm, result));
|
|
}
|
|
|
|
void DevToolsConsoleClient::report_exception(JS::Error const& exception, bool in_promise)
|
|
{
|
|
auto& vm = exception.vm();
|
|
|
|
auto name = exception.get_without_side_effects(vm.names.name);
|
|
auto message = exception.get_without_side_effects(vm.names.message);
|
|
|
|
Vector<WebView::StackFrame> trace;
|
|
trace.ensure_capacity(exception.traceback().size());
|
|
|
|
for (auto const& frame : exception.traceback()) {
|
|
auto const& source_range = frame.source_range();
|
|
WebView::StackFrame stack_frame;
|
|
|
|
if (!frame.function_name.is_empty())
|
|
stack_frame.function = frame.function_name.to_utf8();
|
|
|
|
if (!source_range.filename().is_empty() || source_range.start.offset != 0 || source_range.end.offset != 0) {
|
|
stack_frame.file = String::from_utf8_with_replacement_character(source_range.filename());
|
|
stack_frame.line = source_range.start.line;
|
|
stack_frame.column = source_range.start.column;
|
|
}
|
|
|
|
if (stack_frame.function.has_value() || stack_frame.file.has_value())
|
|
trace.unchecked_append(move(stack_frame));
|
|
}
|
|
|
|
send_console_output({
|
|
.timestamp = UnixDateTime::now(),
|
|
.output = WebView::ConsoleError {
|
|
.name = name.to_string_without_side_effects(),
|
|
.message = message.to_string_without_side_effects(),
|
|
.trace = move(trace),
|
|
.inside_promise = in_promise,
|
|
},
|
|
});
|
|
}
|
|
|
|
void DevToolsConsoleClient::send_console_output(WebView::ConsoleOutput console_output)
|
|
{
|
|
m_client->did_output_js_console_message(move(console_output));
|
|
}
|
|
|
|
// 2.3. Printer(logLevel, args[, options]), https://console.spec.whatwg.org/#printer
|
|
JS::ThrowCompletionOr<JS::Value> DevToolsConsoleClient::printer(JS::Console::LogLevel log_level, PrinterArguments arguments)
|
|
{
|
|
// FIXME: Implement these.
|
|
if (first_is_one_of(log_level, JS::Console::LogLevel::Table, JS::Console::LogLevel::Trace, JS::Console::LogLevel::Group, JS::Console::LogLevel::GroupCollapsed))
|
|
return JS::js_undefined();
|
|
|
|
auto const& argument_values = arguments.get<GC::RootVector<JS::Value>>();
|
|
|
|
auto output = TRY(generically_format_values(argument_values));
|
|
m_console->output_debug_message(log_level, output);
|
|
|
|
Vector<JsonValue> serialized_arguments;
|
|
serialized_arguments.ensure_capacity(argument_values.size());
|
|
|
|
for (auto value : argument_values)
|
|
serialized_arguments.unchecked_append(serialize_js_value(m_console->realm(), value));
|
|
|
|
send_console_output({
|
|
.timestamp = UnixDateTime::now(),
|
|
.output = WebView::ConsoleLog {
|
|
.level = log_level,
|
|
.arguments = move(serialized_arguments),
|
|
},
|
|
});
|
|
|
|
return JS::js_undefined();
|
|
}
|
|
|
|
}
|