Files
browser/src/cdp/domains/runtime.zig
2026-06-15 16:59:04 +08:00

194 lines
6.7 KiB
Zig

// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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");
const Allocator = std.mem.Allocator;
pub fn processMessage(cmd: *CDP.Command) !void {
const action = std.meta.stringToEnum(enum {
enable,
disable,
runIfWaitingForDebugger,
evaluate,
addBinding,
callFunctionOn,
releaseObject,
getProperties,
}, cmd.input.action) orelse return error.UnknownMethod;
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) {
try logInspector(cmd, action);
}
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
// the result to return is handled directly by the inspector.
bc.callInspector(cmd.input.json);
}
fn logInspector(cmd: *CDP.Command, action: anytype) !void {
const script = switch (action) {
.evaluate => blk: {
const params = (try cmd.params(struct {
expression: []const u8,
// contextId: ?u8 = null,
// returnByValue: ?bool = null,
// awaitPromise: ?bool = null,
// userGesture: ?bool = null,
})) orelse return error.InvalidParams;
break :blk params.expression;
},
.callFunctionOn => blk: {
const params = (try cmd.params(struct {
functionDeclaration: []const u8,
// objectId: ?[]const u8 = null,
// executionContextId: ?u8 = null,
// arguments: ?[]struct {
// value: ?[]const u8 = null,
// objectId: ?[]const u8 = null,
// } = null,
// returnByValue: ?bool = null,
// awaitPromise: ?bool = null,
// userGesture: ?bool = null,
})) orelse return error.InvalidParams;
break :blk params.functionDeclaration;
},
else => return,
};
const id = cmd.input.id orelse return error.RequiredId;
const name = try std.fmt.allocPrint(cmd.arena, "id_{d}.js", .{id});
var dir = try std.fs.cwd().makeOpenPath(".zig-cache/tmp", .{});
defer dir.close();
const f = try dir.createFile(name, .{});
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(arena: Allocator, 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);
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.type),
.timestamp = event.timestamp,
.executionContextId = context_id,
.args = args.items,
}, .{ .session_id = session_id });
}
const testing = @import("../testing.zig");
test "cdp.runtime: consoleAPICalled type matches the console method" {
const filter: testing.LogFilter = .init(&.{.js});
defer filter.deinit();
// Wire types per the CDP protocol: console.log -> "log",
// console.warn -> "warning" (not "warn"), console.info -> "info",
// console.error -> "error", console.debug -> "debug".
var ctx = try testing.context();
defer ctx.deinit();
var bc = try ctx.loadBrowserContext(.{ .id = "BID-CONS", .url = "hi.html", .target_id = "FID-0000000CON".* });
try ctx.processMessage(.{ .id = 60, .method = "Runtime.enable" });
const frame = bc.session.currentFrame() orelse unreachable;
var ls: js.Local.Scope = undefined;
frame.js.localScope(&ls);
defer ls.deinit();
_ = try ls.local.exec("console.log('l'); console.warn('w'); console.info('i'); console.error('e'); console.debug('d');", null);
try ctx.expectSentEvent("Runtime.consoleAPICalled", .{ .type = "log" }, .{});
try ctx.expectSentEvent("Runtime.consoleAPICalled", .{ .type = "warning" }, .{});
try ctx.expectSentEvent("Runtime.consoleAPICalled", .{ .type = "info" }, .{});
try ctx.expectSentEvent("Runtime.consoleAPICalled", .{ .type = "error" }, .{});
try ctx.expectSentEvent("Runtime.consoleAPICalled", .{ .type = "debug" }, .{});
}