diff --git a/src/Notification.zig b/src/Notification.zig index 9c2fc045..a85554c2 100644 --- a/src/Notification.zig +++ b/src/Notification.zig @@ -19,6 +19,7 @@ const std = @import("std"); const lp = @import("lightpanda"); +const js = @import("browser/js/js.zig"); const Frame = @import("browser/Frame.zig"); const Transfer = @import("browser/HttpClient.zig").Transfer; const Request = @import("browser/HttpClient.zig").Request; @@ -87,6 +88,7 @@ const EventListeners = struct { http_response_header_done: List = .{}, javascript_dialog_opening: List = .{}, console_message: List = .{}, + runtime_console_message: List = .{}, }; const Events = union(enum) { @@ -108,6 +110,7 @@ const Events = union(enum) { http_response_header_done: *const ResponseHeaderDone, javascript_dialog_opening: *const JavascriptDialogOpening, console_message: *const ConsoleMessage, + runtime_console_message: *const ConsoleMessage, }; const EventType = std.meta.FieldEnum(Events); @@ -226,6 +229,7 @@ pub const DialogResponse = struct { }; pub const ConsoleMessage = struct { + timestamp: u64, source: enum { xml, javascript, @@ -240,7 +244,7 @@ pub const ConsoleMessage = struct { worker, }, level: log.Level, - text: []const u8, + values: []js.Value, url: ?[]const u8 = null, line: ?u32 = null, columns: ?u32 = null, diff --git a/src/browser/webapi/Console.zig b/src/browser/webapi/Console.zig index 00779fe3..54a32400 100644 --- a/src/browser/webapi/Console.zig +++ b/src/browser/webapi/Console.zig @@ -21,6 +21,7 @@ const lp = @import("lightpanda"); const js = @import("../js/js.zig"); const Notification = @import("../../Notification.zig"); +const datetime = @import("../../datetime.zig"); const logger = lp.log; const LogLevel = lp.log.Level; @@ -34,24 +35,21 @@ pub const init: Console = .{}; fn dispatchConsoleMessage(values: []js.Value, level: LogLevel, exec: *js.Execution) void { const notification = exec.context.page.session.notification; - const text = formatValues(values, exec.call_arena) catch return; + const ts = datetime.timestamp(.monotonic); + notification.dispatch(.console_message, &.{ .source = .javascript, .level = level, - .text = text, + .values = values, + .timestamp = ts, }); -} -fn formatValues(values: []js.Value, allocator: std.mem.Allocator) ![]const u8 { - var aw: std.io.Writer.Allocating = .init(allocator); - const w = &aw.writer; - for (values, 0..) |v, i| { - if (i != 0) try w.writeByte(' '); - - const js_str = try v.toString(); - try js_str.format(w); - } - return aw.written(); + notification.dispatch(.runtime_console_message, &.{ + .source = .javascript, + .level = level, + .values = values, + .timestamp = ts, + }); } pub fn trace(_: *const Console, values: []js.Value, exec: *js.Execution) !void { diff --git a/src/cdp/CDP.zig b/src/cdp/CDP.zig index ea792bf4..17d00626 100644 --- a/src/cdp/CDP.zig +++ b/src/cdp/CDP.zig @@ -641,6 +641,14 @@ pub const BrowserContext = struct { self.notification.unregister(.console_message, self); } + pub fn runtimeEnable(self: *BrowserContext) !void { + try self.notification.register(.runtime_console_message, self, onRuntimeConsoleMessage); + } + + pub fn runtimeDisable(self: *BrowserContext) void { + self.notification.unregister(.runtime_console_message, self); + } + pub fn onFrameRemove(ctx: *anyopaque, _: Notification.FrameRemove) !void { const self: *BrowserContext = @ptrCast(@alignCast(ctx)); @import("domains/page.zig").frameRemove(self); @@ -802,11 +810,6 @@ pub const BrowserContext = struct { }; } - pub fn onConsoleMessage(ctx: *anyopaque, msg: *const Notification.ConsoleMessage) !void { - const self: *BrowserContext = @ptrCast(@alignCast(ctx)); - return @import("domains/console.zig").consoleMessage(self, msg); - } - // This is hacky x 2. First, we create the JSON payload by gluing our // session_id onto it. Second, we're much more client/websocket aware than // we should be. @@ -846,6 +849,20 @@ pub const BrowserContext = struct { try cdp.client.sendJSONRaw(buf); } + + pub fn onConsoleMessage(ctx: *anyopaque, msg: *const Notification.ConsoleMessage) !void { + const self: *BrowserContext = @ptrCast(@alignCast(ctx)); + defer self.resetNotificationArena(); + + return @import("domains/console.zig").consoleMessage(self, msg); + } + + pub fn onRuntimeConsoleMessage(ctx: *anyopaque, msg: *const Notification.ConsoleMessage) !void { + const self: *BrowserContext = @ptrCast(@alignCast(ctx)); + defer self.resetNotificationArena(); + + return @import("domains/runtime.zig").consoleMessage(self, msg); + } }; /// see: https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/bindings/core/v8/V8BindingDesign.md#world diff --git a/src/cdp/domains/console.zig b/src/cdp/domains/console.zig index 6100514e..91cfc080 100644 --- a/src/cdp/domains/console.zig +++ b/src/cdp/domains/console.zig @@ -60,9 +60,19 @@ const ConsoleMessage = struct { pub fn consoleMessage(bc: *CDP.BrowserContext, event: *const Notification.ConsoleMessage) !void { const session_id = bc.session_id orelse return; + // format values + var aw: std.io.Writer.Allocating = .init(bc.notification_arena); + const w = &aw.writer; + for (event.values, 0..) |v, i| { + if (i != 0) try w.writeByte(' '); + + const js_str = try v.toString(); + try js_str.format(w); + } + return bc.cdp.sendEvent("Console.messageAdded", ConsoleMessage{ .source = @tagName(event.source), .level = @tagName(event.level), - .text = event.text, + .text = aw.written(), }, .{ .session_id = session_id }); } diff --git a/src/cdp/domains/runtime.zig b/src/cdp/domains/runtime.zig index 96b142af..44e8762e 100644 --- a/src/cdp/domains/runtime.zig +++ b/src/cdp/domains/runtime.zig @@ -19,11 +19,14 @@ const std = @import("std"); const builtin = @import("builtin"); +const js = @import("../../browser/js/js.zig"); const CDP = @import("../CDP.zig"); +const Notification = @import("../../Notification.zig"); pub fn processMessage(cmd: *CDP.Command) !void { const action = std.meta.stringToEnum(enum { enable, + disable, runIfWaitingForDebugger, evaluate, addBinding, @@ -34,10 +37,24 @@ pub fn processMessage(cmd: *CDP.Command) !void { switch (action) { .runIfWaitingForDebugger => return cmd.sendResult(null, .{}), + .enable => return enable(cmd), + .disable => return disable(cmd), else => return sendInspector(cmd, action), } } +fn enable(cmd: *CDP.Command) !void { + const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; + try bc.runtimeEnable(); + return sendInspector(cmd, .enable); +} + +fn disable(cmd: *CDP.Command) !void { + const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; + bc.runtimeDisable(); + return sendInspector(cmd, .disable); +} + fn sendInspector(cmd: *CDP.Command, action: anytype) !void { // save script in file at debug mode if (builtin.mode == .Debug) { @@ -91,3 +108,57 @@ fn logInspector(cmd: *CDP.Command, action: anytype) !void { defer f.close(); try f.writeAll(script); } + +const RemoteObject = struct { + type: []const u8, + subtype: ?[]const u8, + className: ?[]const u8, + description: ?[]const u8, + objectId: ?[]const u8, + value: js.Value, +}; + +const ConsoleMessage = struct { + type: []const u8, + executionContextId: i32, + timestamp: u64, + args: []RemoteObject, +}; + +pub fn consoleMessage(bc: *CDP.BrowserContext, event: *const Notification.ConsoleMessage) !void { + const session_id = bc.session_id orelse return; + const frame = bc.session.currentFrame() orelse return error.FrameNotLoaded; + + var ls: js.Local.Scope = undefined; + frame.js.localScope(&ls); + defer ls.deinit(); + + const context_id = bc.inspector_session.inspector.getContextId(&ls.local); + const arena = bc.notification_arena; + + var args: std.ArrayList(RemoteObject) = .empty; + for (event.values) |value| { + const remote_object = try bc.inspector_session.getRemoteObject( + &ls.local, + "", + value, + ); + defer remote_object.deinit(); + + try args.append(arena, .{ + .type = try remote_object.getType(arena), + .subtype = try remote_object.getSubtype(arena), + .className = try remote_object.getClassName(arena), + .description = try remote_object.getDescription(arena), + .objectId = try remote_object.getObjectId(arena), + .value = value, + }); + } + + return bc.cdp.sendEvent("Runtime.consoleAPICalled", ConsoleMessage{ + .type = @tagName(event.level), + .timestamp = event.timestamp, + .executionContextId = context_id, + .args = args.items, + }, .{ .session_id = session_id }); +}