Refactor CDP

CDP is now an struct which contains its own state a browser and a session.

When a client connection is made and successfully upgrades, the client creates
the CDP instance. There is now a cleaner separation betwen Server, Client and
CDP.

Removed a number of allocations, especially when writing results/events from
CDP to the client. Improved input message parsing. Tried to remove some usage
of undefined.
This commit is contained in:
Karl Seguin
2025-02-10 17:34:54 +08:00
parent 14fe4f65e1
commit 6ab64d155b
20 changed files with 1305 additions and 2202 deletions

View File

@@ -19,6 +19,8 @@
const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
const Types = @import("root").Types;
const parser = @import("netsurf");
@@ -57,30 +59,44 @@ pub const user_agent = "Lightpanda/1.0";
// A browser contains only one session.
// TODO allow multiple sessions per browser.
pub const Browser = struct {
session: Session = undefined,
agent: []const u8 = user_agent,
loop: *Loop,
session: ?*Session,
allocator: Allocator,
session_pool: SessionPool,
const SessionPool = std.heap.MemoryPool(Session);
const uri = "about:blank";
pub fn init(self: *Browser, alloc: std.mem.Allocator, loop: *Loop, vm: jsruntime.VM) !void {
// We want to ensure the caller initialised a VM, but the browser
// doesn't use it directly...
_ = vm;
try Session.init(&self.session, alloc, loop, uri);
pub fn init(allocator: Allocator, loop: *Loop) Browser {
return .{
.loop = loop,
.session = null,
.allocator = allocator,
.session_pool = SessionPool.init(allocator),
};
}
pub fn deinit(self: *Browser) void {
self.session.deinit();
self.closeSession();
self.session_pool.deinit();
}
pub fn newSession(
self: *Browser,
alloc: std.mem.Allocator,
loop: *jsruntime.Loop,
) !void {
self.session.deinit();
try Session.init(&self.session, alloc, loop, uri);
pub fn newSession(self: *Browser, ctx: anytype) !*Session {
self.closeSession();
const session = try self.session_pool.create();
try Session.init(session, self.allocator, ctx, self.loop, uri);
self.session = session;
return session;
}
fn closeSession(self: *Browser) void {
if (self.session) |session| {
session.deinit();
self.session_pool.destroy(session);
self.session = null;
}
}
};
@@ -90,7 +106,7 @@ pub const Browser = struct {
// deinit a page before running another one.
pub const Session = struct {
// allocator used to init the arena.
alloc: std.mem.Allocator,
allocator: Allocator,
// The arena is used only to bound the js env init b/c it leaks memory.
// see https://github.com/lightpanda-io/jsruntime-lib/issues/181
@@ -103,8 +119,9 @@ pub const Session = struct {
// TODO handle proxy
loader: Loader,
env: Env = undefined,
inspector: ?jsruntime.Inspector = null,
env: Env,
inspector: jsruntime.Inspector,
window: Window,
@@ -115,20 +132,54 @@ pub const Session = struct {
jstypes: [Types.len]usize = undefined,
fn init(self: *Session, alloc: std.mem.Allocator, loop: *Loop, uri: []const u8) !void {
self.* = Session{
fn init(self: *Session, allocator: Allocator, ctx: anytype, loop: *Loop, uri: []const u8) !void {
self.* = .{
.uri = uri,
.alloc = alloc,
.arena = std.heap.ArenaAllocator.init(alloc),
.env = undefined,
.inspector = undefined,
.allocator = allocator,
.loader = Loader.init(allocator),
.httpClient = .{ .allocator = allocator },
.storageShed = storage.Shed.init(allocator),
.arena = std.heap.ArenaAllocator.init(allocator),
.window = Window.create(null, .{ .agent = user_agent }),
.loader = Loader.init(alloc),
.storageShed = storage.Shed.init(alloc),
.httpClient = undefined,
};
Env.init(&self.env, self.arena.allocator(), loop, null);
self.httpClient = .{ .allocator = alloc };
const arena = self.arena.allocator();
Env.init(&self.env, arena, loop, null);
errdefer self.env.deinit();
try self.env.load(&self.jstypes);
const ContextT = @TypeOf(ctx);
const InspectorContainer = switch (@typeInfo(ContextT)) {
.Struct => ContextT,
.Pointer => |ptr| ptr.child,
.Void => NoopInspector,
else => @compileError("invalid context type"),
};
// const ctx_opaque = @as(*anyopaque, @ptrCast(ctx));
self.inspector = try jsruntime.Inspector.init(
arena,
self.env,
if (@TypeOf(ctx) == void) @constCast(@ptrCast(&{})) else ctx,
InspectorContainer.onInspectorResponse,
InspectorContainer.onInspectorEvent,
);
self.env.setInspector(self.inspector);
}
fn deinit(self: *Session) void {
if (self.page) |*p| {
p.end();
}
self.env.deinit();
self.arena.deinit();
self.httpClient.deinit();
self.loader.deinit();
self.storageShed.deinit();
}
fn fetchModule(ctx: *anyopaque, referrer: ?jsruntime.Module, specifier: []const u8) !jsruntime.Module {
@@ -146,47 +197,15 @@ pub const Session = struct {
return self.env.compileModule(body, specifier);
}
fn deinit(self: *Session) void {
if (self.page) |*p| p.end();
if (self.inspector) |inspector| {
inspector.deinit(self.alloc);
}
self.env.deinit();
self.arena.deinit();
self.httpClient.deinit();
self.loader.deinit();
self.storageShed.deinit();
}
pub fn initInspector(
self: *Session,
ctx: anytype,
onResp: jsruntime.InspectorOnResponseFn,
onEvent: jsruntime.InspectorOnEventFn,
) !void {
const ctx_opaque = @as(*anyopaque, @ptrCast(ctx));
self.inspector = try jsruntime.Inspector.init(self.alloc, self.env, ctx_opaque, onResp, onEvent);
self.env.setInspector(self.inspector.?);
}
pub fn callInspector(self: *Session, msg: []const u8) void {
if (self.inspector) |inspector| {
inspector.send(msg, self.env);
} else {
@panic("No Inspector");
}
self.inspector.send(self.env, msg);
}
// NOTE: the caller is not the owner of the returned value,
// the pointer on Page is just returned as a convenience
pub fn createPage(self: *Session) !*Page {
if (self.page != null) return error.SessionPageExists;
const p: Page = undefined;
self.page = p;
Page.init(&self.page.?, self.alloc, self);
self.page = Page.init(self.allocator, self);
return &self.page.?;
}
};
@@ -197,8 +216,8 @@ pub const Session = struct {
// The page handle all its memory in an arena allocator. The arena is reseted
// when end() is called.
pub const Page = struct {
arena: std.heap.ArenaAllocator,
session: *Session,
arena: std.heap.ArenaAllocator,
doc: ?*parser.Document = null,
// handle url
@@ -212,17 +231,18 @@ pub const Page = struct {
raw_data: ?[]const u8 = null,
fn init(
self: *Page,
alloc: std.mem.Allocator,
session: *Session,
) void {
self.* = .{
.arena = std.heap.ArenaAllocator.init(alloc),
fn init(allocator: Allocator, session: *Session) Page {
return .{
.session = session,
.arena = std.heap.ArenaAllocator.init(allocator),
};
}
pub fn deinit(self: *Page) void {
self.arena.deinit();
self.session.page = null;
}
// start js env.
// - auxData: extra data forwarded to the Inspector
// see Inspector.contextCreated
@@ -242,10 +262,8 @@ pub const Page = struct {
try polyfill.load(self.arena.allocator(), self.session.env);
// inspector
if (self.session.inspector) |inspector| {
log.debug("inspector context created", .{});
inspector.contextCreated(self.session.env, "", self.origin orelse "://", auxData);
}
log.debug("inspector context created", .{});
self.session.inspector.contextCreated(self.session.env, "", self.origin orelse "://", auxData);
}
// reset js env and mem arena.
@@ -253,7 +271,6 @@ pub const Page = struct {
self.session.env.stop();
// TODO unload document: https://html.spec.whatwg.org/#unloading-documents
if (self.url) |*u| u.deinit(self.arena.allocator());
self.url = null;
self.location.url = null;
self.session.window.replaceLocation(&self.location) catch |e| {
@@ -266,13 +283,8 @@ pub const Page = struct {
_ = self.arena.reset(.free_all);
}
pub fn deinit(self: *Page) void {
self.arena.deinit();
self.session.page = null;
}
// dump writes the page content into the given file.
pub fn dump(self: *Page, out: std.fs.File) !void {
pub fn dump(self: *const Page, out: std.fs.File) !void {
// if no HTML document pointer available, dump the data content only.
if (self.doc == null) {
@@ -320,11 +332,9 @@ pub const Page = struct {
}
// own the url
if (self.rawuri) |prev| alloc.free(prev);
self.rawuri = try alloc.dupe(u8, uri);
self.uri = std.Uri.parse(self.rawuri.?) catch try std.Uri.parseAfterScheme("", self.rawuri.?);
if (self.url) |*prev| prev.deinit(alloc);
self.url = try URL.constructor(alloc, self.rawuri.?, null);
self.location.url = &self.url.?;
try self.session.window.replaceLocation(&self.location);
@@ -422,9 +432,7 @@ pub const Page = struct {
// https://html.spec.whatwg.org/#read-html
// inspector
if (self.session.inspector) |inspector| {
inspector.contextCreated(self.session.env, "", self.origin.?, auxData);
}
self.session.inspector.contextCreated(self.session.env, "", self.origin.?, auxData);
// replace the user context document with the new one.
try self.session.env.setUserContext(.{
@@ -583,7 +591,7 @@ pub const Page = struct {
};
// the caller owns the returned string
fn fetchData(self: *Page, alloc: std.mem.Allocator, src: []const u8) ![]const u8 {
fn fetchData(self: *Page, alloc: Allocator, src: []const u8) ![]const u8 {
log.debug("starting fetch {s}", .{src});
var buffer: [1024]u8 = undefined;
@@ -658,7 +666,7 @@ pub const Page = struct {
return .unknown;
}
fn eval(self: Script, alloc: std.mem.Allocator, env: Env, body: []const u8) !void {
fn eval(self: Script, alloc: Allocator, env: Env, body: []const u8) !void {
var try_catch: jsruntime.TryCatch = undefined;
try_catch.init(env);
defer try_catch.deinit();
@@ -683,3 +691,8 @@ pub const Page = struct {
}
};
};
const NoopInspector = struct {
pub fn onInspectorResponse(_: *anyopaque, _: u32, _: []const u8) void {}
pub fn onInspectorEvent(_: *anyopaque, _: []const u8) void {}
};

View File

@@ -17,132 +17,66 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const server = @import("../server.zig");
const Ctx = server.Ctx;
const cdp = @import("cdp.zig");
const result = cdp.result;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const log = std.log.scoped(.cdp);
const Methods = enum {
getVersion,
setDownloadBehavior,
getWindowForTarget,
setWindowBounds,
};
pub fn browser(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
action: []const u8,
ctx: *Ctx,
) ![]const u8 {
const method = std.meta.stringToEnum(Methods, action) orelse
return error.UnknownMethod;
return switch (method) {
.getVersion => getVersion(alloc, msg, ctx),
.setDownloadBehavior => setDownloadBehavior(alloc, msg, ctx),
.getWindowForTarget => getWindowForTarget(alloc, msg, ctx),
.setWindowBounds => setWindowBounds(alloc, msg, ctx),
};
}
// TODO: hard coded data
const ProtocolVersion = "1.3";
const Product = "Chrome/124.0.6367.29";
const Revision = "@9e6ded5ac1ff5e38d930ae52bd9aec09bd1a68e4";
const UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
const JsVersion = "12.4.254.8";
const PROTOCOL_VERSION = "1.3";
const PRODUCT = "Chrome/124.0.6367.29";
const REVISION = "@9e6ded5ac1ff5e38d930ae52bd9aec09bd1a68e4";
const USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
const JS_VERSION = "12.4.254.8";
const DEV_TOOLS_WINDOW_ID = 1923710101;
fn getVersion(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "browser.getVersion" });
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
getVersion,
setDownloadBehavior,
getWindowForTarget,
setWindowBounds,
}, cmd.action) orelse return error.UnknownMethod;
// ouput
const Res = struct {
protocolVersion: []const u8 = ProtocolVersion,
product: []const u8 = Product,
revision: []const u8 = Revision,
userAgent: []const u8 = UserAgent,
jsVersion: []const u8 = JsVersion,
};
return result(alloc, input.id, Res, .{}, null);
switch (action) {
.getVersion => return getVersion(cmd),
.setDownloadBehavior => return setDownloadBehavior(cmd),
.getWindowForTarget => return getWindowForTarget(cmd),
.setWindowBounds => return setWindowBounds(cmd),
}
}
fn getVersion(cmd: anytype) !void {
// TODO: pre-serialize?
return cmd.sendResult(.{
.protocolVersion = PROTOCOL_VERSION,
.product = PRODUCT,
.revision = REVISION,
.userAgent = USER_AGENT,
.jsVersion = JS_VERSION,
}, .{ .include_session_id = false });
}
// TODO: noop method
fn setDownloadBehavior(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const Params = struct {
behavior: []const u8,
browserContextId: ?[]const u8 = null,
downloadPath: ?[]const u8 = null,
eventsEnabled: ?bool = null,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("REQ > id {d}, method {s}", .{ input.id, "browser.setDownloadBehavior" });
fn setDownloadBehavior(cmd: anytype) !void {
// const params = (try cmd.params(struct {
// behavior: []const u8,
// browserContextId: ?[]const u8 = null,
// downloadPath: ?[]const u8 = null,
// eventsEnabled: ?bool = null,
// })) orelse return error.InvalidParams;
// output
return result(alloc, input.id, null, null, null);
return cmd.sendResult(null, .{ .include_session_id = false });
}
// TODO: hard coded ID
const DevToolsWindowID = 1923710101;
fn getWindowForTarget(cmd: anytype) !void {
// const params = (try cmd.params(struct {
// targetId: ?[]const u8 = null,
// })) orelse return error.InvalidParams;
fn getWindowForTarget(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const Params = struct {
targetId: ?[]const u8 = null,
};
const input = try Input(?Params).get(alloc, msg);
defer input.deinit();
std.debug.assert(input.sessionId != null);
log.debug("Req > id {d}, method {s}", .{ input.id, "browser.getWindowForTarget" });
// output
const Resp = struct {
windowId: u64 = DevToolsWindowID,
bounds: struct {
left: ?u64 = null,
top: ?u64 = null,
width: ?u64 = null,
height: ?u64 = null,
windowState: []const u8 = "normal",
} = .{},
};
return result(alloc, input.id, Resp, Resp{}, input.sessionId);
return cmd.sendResult(.{ .windowId = DEV_TOOLS_WINDOW_ID, .bounds = .{
.windowState = "normal",
} }, .{});
}
// TODO: noop method
fn setWindowBounds(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "browser.setWindowBounds" });
// output
return result(alloc, input.id, null, null, input.sessionId);
fn setWindowBounds(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}

View File

@@ -17,209 +17,387 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Allocator = std.mem.Allocator;
const json = std.json;
const server = @import("../server.zig");
const Ctx = server.Ctx;
const Loop = @import("jsruntime").Loop;
const Client = @import("../server.zig").Client;
const asUint = @import("../str/parser.zig").asUint;
const Browser = @import("../browser/browser.zig").Browser;
const Session = @import("../browser/browser.zig").Session;
const browser = @import("browser.zig").browser;
const target = @import("target.zig").target;
const page = @import("page.zig").page;
const log = @import("log.zig").log;
const runtime = @import("runtime.zig").runtime;
const network = @import("network.zig").network;
const emulation = @import("emulation.zig").emulation;
const fetch = @import("fetch.zig").fetch;
const performance = @import("performance.zig").performance;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const inspector = @import("inspector.zig").inspector;
const dom = @import("dom.zig").dom;
const css = @import("css.zig").css;
const security = @import("security.zig").security;
const log = std.log.scoped(.cdp);
const log_cdp = std.log.scoped(.cdp);
pub const URL_BASE = "chrome://newtab/";
pub const LOADER_ID = "LOADERID24DD2FD56CF1EF33C965C79C";
pub const FRAME_ID = "FRAMEIDD8AED408A0467AC93100BCDBE";
pub const BROWSER_SESSION_ID = @tagName(SessionID.BROWSERSESSIONID597D9875C664CAC0);
pub const CONTEXT_SESSION_ID = @tagName(SessionID.CONTEXTSESSIONID0497A05C95417CF4);
pub const Error = error{
UnknonwDomain,
UnknownMethod,
NoResponse,
RequestWithoutID,
pub const TimestampEvent = struct {
timestamp: f64,
};
pub fn isCdpError(err: anyerror) ?Error {
// see https://github.com/ziglang/zig/issues/2473
const errors = @typeInfo(Error).ErrorSet.?;
inline for (errors) |e| {
if (std.mem.eql(u8, e.name, @errorName(err))) {
return @errorCast(err);
}
pub const CDP = struct {
// Used for sending message to the client and closing on error
client: *Client,
// The active browser
browser: Browser,
// The active browser session
session: ?*Session,
allocator: Allocator,
// Re-used arena for processing a message. We're assuming that we're getting
// 1 message at a time.
message_arena: std.heap.ArenaAllocator,
// State
url: []const u8,
frame_id: []const u8,
loader_id: []const u8,
session_id: SessionID,
context_id: ?[]const u8,
execution_context_id: u32,
security_origin: []const u8,
page_life_cycle_events: bool,
secure_context_type: []const u8,
pub fn init(allocator: Allocator, client: *Client, loop: *Loop) CDP {
return .{
.client = client,
.browser = Browser.init(allocator, loop),
.session = null,
.allocator = allocator,
.url = URL_BASE,
.execution_context_id = 0,
.context_id = null,
.frame_id = FRAME_ID,
.session_id = .CONTEXTSESSIONID0497A05C95417CF4,
.security_origin = URL_BASE,
.secure_context_type = "Secure", // TODO = enum
.loader_id = LOADER_ID,
.message_arena = std.heap.ArenaAllocator.init(allocator),
.page_life_cycle_events = false, // TODO; Target based value
};
}
return null;
}
const Domains = enum {
Browser,
Target,
Page,
Log,
Runtime,
Network,
DOM,
CSS,
Inspector,
Emulation,
Fetch,
Performance,
Security,
pub fn deinit(self: *CDP) void {
self.browser.deinit();
self.message_arena.deinit();
}
pub fn newSession(self: *CDP) !void {
self.session = try self.browser.newSession(self);
}
pub fn processMessage(self: *CDP, msg: []const u8) void {
const arena = &self.message_arena;
defer _ = arena.reset(.{ .retain_with_limit = 1024 * 16 });
self.dispatch(arena.allocator(), self, msg) catch |err| {
log.err("failed to process message: {}\n{s}", .{ err, msg });
self.client.close(null);
return;
};
}
// Called from above, in processMessage which handles client messages
// but can also be called internally. For example, Target.sendMessageToTarget
// calls back into dispatch.
pub fn dispatch(
self: *CDP,
arena: Allocator,
sender: anytype,
str: []const u8,
) anyerror!void {
const input = try json.parseFromSliceLeaky(InputMessage, arena, str, .{
.ignore_unknown_fields = true,
});
const domain, const action = blk: {
const method = input.method;
const i = std.mem.indexOfScalarPos(u8, method, 0, '.') orelse {
return error.InvalidMethod;
};
break :blk .{ method[0..i], method[i + 1 ..] };
};
var command = Command(@TypeOf(sender)){
.json = str,
.cdp = self,
.id = input.id,
.arena = arena,
.action = action,
._params = input.params,
.session_id = input.sessionId,
.sender = sender,
.session = self.session orelse blk: {
try self.newSession();
break :blk self.session.?;
},
};
switch (domain.len) {
3 => switch (@as(u24, @bitCast(domain[0..3].*))) {
asUint("DOM") => return @import("dom.zig").processMessage(&command),
asUint("Log") => return @import("log.zig").processMessage(&command),
asUint("CSS") => return @import("css.zig").processMessage(&command),
else => {},
},
4 => switch (@as(u32, @bitCast(domain[0..4].*))) {
asUint("Page") => return @import("page.zig").processMessage(&command),
else => {},
},
5 => switch (@as(u40, @bitCast(domain[0..5].*))) {
asUint("Fetch") => return @import("fetch.zig").processMessage(&command),
else => {},
},
6 => switch (@as(u48, @bitCast(domain[0..6].*))) {
asUint("Target") => return @import("target.zig").processMessage(&command),
else => {},
},
7 => switch (@as(u56, @bitCast(domain[0..7].*))) {
asUint("Browser") => return @import("browser.zig").processMessage(&command),
asUint("Runtime") => return @import("runtime.zig").processMessage(&command),
asUint("Network") => return @import("network.zig").processMessage(&command),
else => {},
},
8 => switch (@as(u64, @bitCast(domain[0..8].*))) {
asUint("Security") => return @import("security.zig").processMessage(&command),
else => {},
},
9 => switch (@as(u72, @bitCast(domain[0..9].*))) {
asUint("Emulation") => return @import("emulation.zig").processMessage(&command),
asUint("Inspector") => return @import("inspector.zig").processMessage(&command),
else => {},
},
11 => switch (@as(u88, @bitCast(domain[0..11].*))) {
asUint("Performance") => return @import("performance.zig").processMessage(&command),
else => {},
},
else => {},
}
return error.UnknownDomain;
}
fn sendJSON(self: *CDP, message: anytype) !void {
return self.client.sendJSON(message, .{
.emit_null_optional_fields = false,
});
}
pub fn onInspectorResponse(ctx: *anyopaque, _: u32, msg: []const u8) void {
if (std.log.defaultLogEnabled(.debug)) {
// msg should be {"id":<id>,...
std.debug.assert(std.mem.startsWith(u8, msg, "{\"id\":"));
const id_end = std.mem.indexOfScalar(u8, msg, ',') orelse {
log.warn("invalid inspector response message: {s}", .{msg});
return;
};
const id = msg[6..id_end];
log.debug("Res (inspector) > id {s}", .{id});
}
sendInspectorMessage(@alignCast(@ptrCast(ctx)), msg) catch |err| {
log.err("Failed to send inspector response: {any}", .{err});
};
}
pub fn onInspectorEvent(ctx: *anyopaque, msg: []const u8) void {
if (std.log.defaultLogEnabled(.debug)) {
// msg should be {"method":<method>,...
std.debug.assert(std.mem.startsWith(u8, msg, "{\"method\":"));
const method_end = std.mem.indexOfScalar(u8, msg, ',') orelse {
log.warn("invalid inspector event message: {s}", .{msg});
return;
};
const method = msg[10..method_end];
log.debug("Event (inspector) > method {s}", .{method});
}
sendInspectorMessage(@alignCast(@ptrCast(ctx)), msg) catch |err| {
log.err("Failed to send inspector event: {any}", .{err});
};
}
// This is hacky * 2. First, we have the JSON payload by gluing our
// session_id onto it. Second, we're much more client/websocket aware than
// we should be.
fn sendInspectorMessage(self: *CDP, msg: []const u8) !void {
var arena = std.heap.ArenaAllocator.init(self.allocator);
errdefer arena.deinit();
const field = ",\"sessionId\":\"";
const session_id = @tagName(self.session_id);
// + 1 for the closing quote after the session id
// + 10 for the max websocket header
const message_len = msg.len + session_id.len + 1 + field.len + 10;
var buf: std.ArrayListUnmanaged(u8) = .{};
buf.ensureTotalCapacity(arena.allocator(), message_len) catch |err| {
log.err("Failed to expand inspector buffer: {any}", .{err});
return;
};
// reserve 10 bytes for websocket header
buf.appendSliceAssumeCapacity(&.{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 });
// -1 because we dont' want the closing brace '}'
buf.appendSliceAssumeCapacity(msg[0 .. msg.len - 1]);
buf.appendSliceAssumeCapacity(field);
buf.appendSliceAssumeCapacity(session_id);
buf.appendSliceAssumeCapacity("\"}");
std.debug.assert(buf.items.len == message_len);
try self.client.sendJSONRaw(arena, buf);
}
};
// The caller is responsible for calling `free` on the returned slice.
pub fn do(
alloc: std.mem.Allocator,
s: []const u8,
ctx: *Ctx,
) anyerror![]const u8 {
// This is a generic because when we send a result we have two different
// behaviors. Normally, we're sending the result to the client. But in some cases
// we want to capture the result. So we want the command.sendResult to be
// generic.
pub fn Command(comptime Sender: type) type {
return struct {
// refernece to our CDP instance
cdp: *CDP,
// incoming message parser
var msg = IncomingMessage.init(alloc, s);
defer msg.deinit();
// Comes directly from the input.id field
id: ?i64,
return dispatch(alloc, &msg, ctx);
}
// A misc arena that can be used for any allocation for processing
// the message
arena: Allocator,
pub fn dispatch(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) anyerror![]const u8 {
const method = try msg.getMethod();
// the browser session
session: *Session,
// retrieve domain from method
var iter = std.mem.splitScalar(u8, method, '.');
const domain = std.meta.stringToEnum(Domains, iter.first()) orelse
return error.UnknonwDomain;
// The "action" of the message.Given a method of "LOG.enable", the
// action is "enable"
action: []const u8,
// select corresponding domain
const action = iter.next() orelse return error.BadMethod;
return switch (domain) {
.Browser => browser(alloc, msg, action, ctx),
.Target => target(alloc, msg, action, ctx),
.Page => page(alloc, msg, action, ctx),
.Log => log(alloc, msg, action, ctx),
.Runtime => runtime(alloc, msg, action, ctx),
.Network => network(alloc, msg, action, ctx),
.DOM => dom(alloc, msg, action, ctx),
.CSS => css(alloc, msg, action, ctx),
.Inspector => inspector(alloc, msg, action, ctx),
.Emulation => emulation(alloc, msg, action, ctx),
.Fetch => fetch(alloc, msg, action, ctx),
.Performance => performance(alloc, msg, action, ctx),
.Security => security(alloc, msg, action, ctx),
// Comes directly from the input.sessionId field
session_id: ?[]const u8,
// Unparsed / untyped input.params.
_params: ?InputParams,
// The full raw json input
json: []const u8,
sender: Sender,
const Self = @This();
pub fn params(self: *const Self, comptime T: type) !?T {
if (self._params) |p| {
return try json.parseFromSliceLeaky(
T,
self.arena,
p.raw,
.{ .ignore_unknown_fields = true },
);
}
return null;
}
const SendResultOpts = struct {
include_session_id: bool = true,
};
pub fn sendResult(self: *Self, result: anytype, opts: SendResultOpts) !void {
return self.sender.sendJSON(.{
.id = self.id,
.result = if (comptime @typeInfo(@TypeOf(result)) == .Null) struct {}{} else result,
.sessionId = if (opts.include_session_id) self.session_id else null,
});
}
const SendEventOpts = struct {
session_id: ?[]const u8 = null,
};
pub fn sendEvent(self: *Self, method: []const u8, p: anytype, opts: SendEventOpts) !void {
// Events ALWAYS go to the client. self.sender should not be used
return self.cdp.sendJSON(.{
.method = method,
.params = if (comptime @typeInfo(@TypeOf(p)) == .Null) struct {}{} else p,
.sessionId = opts.session_id,
});
}
};
}
pub const State = struct {
executionContextId: u32 = 0,
contextID: ?[]const u8 = null,
sessionID: SessionID = .CONTEXTSESSIONID0497A05C95417CF4,
frameID: []const u8 = FrameID,
url: []const u8 = URLBase,
securityOrigin: []const u8 = URLBase,
secureContextType: []const u8 = "Secure", // TODO: enum
loaderID: []const u8 = LoaderID,
// When we parse a JSON message from the client, this is the structure
// we always expect
const InputMessage = struct {
id: ?i64,
method: []const u8,
params: ?InputParams = null,
sessionId: ?[]const u8 = null,
};
page_life_cycle_events: bool = false, // TODO; Target based value
// The JSON "params" field changes based on the "method". Initially, we just
// capture the raw json object (including the opening and closing braces).
// Then, when we're processing the message, and we know what type it is, we
// can parse it (in Disaptch(T).params).
const InputParams = struct {
raw: []const u8,
pub fn jsonParse(
_: Allocator,
scanner: *json.Scanner,
_: json.ParseOptions,
) !InputParams {
const height = scanner.stackHeight();
const start = scanner.cursor;
if (try scanner.next() != .object_begin) {
return error.UnexpectedToken;
}
try scanner.skipUntilStackHeight(height);
const end = scanner.cursor;
return .{ .raw = scanner.input[start..end] };
}
};
// Utils
// -----
pub fn dumpFile(
alloc: std.mem.Allocator,
id: u16,
script: []const u8,
) !void {
const name = try std.fmt.allocPrint(alloc, "id_{d}.js", .{id});
defer alloc.free(name);
var dir = try std.fs.cwd().makeOpenPath("zig-cache/tmp", .{});
defer dir.close();
const f = try dir.createFile(name, .{});
defer f.close();
const nb = try f.write(script);
std.debug.assert(nb == script.len);
const p = try dir.realpathAlloc(alloc, name);
defer alloc.free(p);
}
// pub fn dumpFile(
// alloc: std.mem.Allocator,
// id: u16,
// script: []const u8,
// ) !void {
// const name = try std.fmt.allocPrint(alloc, "id_{d}.js", .{id});
// defer alloc.free(name);
// var dir = try std.fs.cwd().makeOpenPath("zig-cache/tmp", .{});
// defer dir.close();
// const f = try dir.createFile(name, .{});
// defer f.close();
// const nb = try f.write(script);
// std.debug.assert(nb == script.len);
// const p = try dir.realpathAlloc(alloc, name);
// defer alloc.free(p);
// }
// caller owns the slice returned
pub fn stringify(alloc: std.mem.Allocator, res: anytype) ![]const u8 {
var out = std.ArrayList(u8).init(alloc);
defer out.deinit();
// // caller owns the slice returned
// pub fn stringify(alloc: std.mem.Allocator, res: anytype) ![]const u8 {
// var out = std.ArrayList(u8).init(alloc);
// defer out.deinit();
// Do not emit optional null fields
const options: std.json.StringifyOptions = .{ .emit_null_optional_fields = false };
// // Do not emit optional null fields
// const options: std.json.StringifyOptions = .{ .emit_null_optional_fields = false };
try std.json.stringify(res, options, out.writer());
const ret = try alloc.alloc(u8, out.items.len);
@memcpy(ret, out.items);
return ret;
}
const resultNull = "{{\"id\": {d}, \"result\": {{}}}}";
const resultNullSession = "{{\"id\": {d}, \"result\": {{}}, \"sessionId\": \"{s}\"}}";
// caller owns the slice returned
pub fn result(
alloc: std.mem.Allocator,
id: u16,
comptime T: ?type,
res: anytype,
sessionID: ?[]const u8,
) ![]const u8 {
log_cdp.debug(
"Res > id {d}, sessionID {?s}, result {any}",
.{ id, sessionID, res },
);
if (T == null) {
// No need to stringify a custom JSON msg, just use string templates
if (sessionID) |sID| {
return try std.fmt.allocPrint(alloc, resultNullSession, .{ id, sID });
}
return try std.fmt.allocPrint(alloc, resultNull, .{id});
}
const Resp = struct {
id: u16,
result: T.?,
sessionId: ?[]const u8,
};
const resp = Resp{ .id = id, .result = res, .sessionId = sessionID };
return stringify(alloc, resp);
}
pub fn sendEvent(
alloc: std.mem.Allocator,
ctx: *Ctx,
name: []const u8,
comptime T: type,
params: T,
sessionID: ?[]const u8,
) !void {
// some clients like chromedp expects empty parameters structs.
if (T == void) @compileError("sendEvent: use struct{} instead of void for empty parameters");
log_cdp.debug("Event > method {s}, sessionID {?s}", .{ name, sessionID });
const Resp = struct {
method: []const u8,
params: T,
sessionId: ?[]const u8,
};
const resp = Resp{ .method = name, .params = params, .sessionId = sessionID };
const event_msg = try stringify(alloc, resp);
try ctx.send(event_msg);
}
// try std.json.stringify(res, options, out.writer());
// const ret = try alloc.alloc(u8, out.items.len);
// @memcpy(ret, out.items);
// return ret;
// }
// Common
// ------
@@ -230,20 +408,9 @@ pub const SessionID = enum {
CONTEXTSESSIONID0497A05C95417CF4,
pub fn parse(str: []const u8) !SessionID {
inline for (@typeInfo(SessionID).Enum.fields) |enumField| {
if (std.mem.eql(u8, str, enumField.name)) {
return @field(SessionID, enumField.name);
}
}
return error.InvalidSessionID;
return std.meta.stringToEnum(SessionID, str) orelse {
log.err("parse sessionID: {s}", .{str});
return error.InvalidSessionID;
};
}
};
pub const BrowserSessionID = @tagName(SessionID.BROWSERSESSIONID597D9875C664CAC0);
pub const ContextSessionID = @tagName(SessionID.CONTEXTSESSIONID0497A05C95417CF4);
pub const URLBase = "chrome://newtab/";
pub const LoaderID = "LOADERID24DD2FD56CF1EF33C965C79C";
pub const FrameID = "FRAMEIDD8AED408A0467AC93100BCDBE";
pub const TimestampEvent = struct {
timestamp: f64,
};

View File

@@ -17,43 +17,14 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const server = @import("../server.zig");
const Ctx = server.Ctx;
const cdp = @import("cdp.zig");
const result = cdp.result;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const log = std.log.scoped(.cdp);
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
enable,
}, cmd.action) orelse return error.UnknownMethod;
const Methods = enum {
enable,
};
pub fn css(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
action: []const u8,
ctx: *Ctx,
) ![]const u8 {
const method = std.meta.stringToEnum(Methods, action) orelse
return error.UnknownMethod;
return switch (method) {
.enable => enable(alloc, msg, ctx),
};
}
fn enable(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "inspector.enable" });
return result(alloc, input.id, null, null, input.sessionId);
switch (action) {
.enable => return cmd.sendResult(null, .{}),
}
}

View File

@@ -17,43 +17,14 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const server = @import("../server.zig");
const Ctx = server.Ctx;
const cdp = @import("cdp.zig");
const result = cdp.result;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const log = std.log.scoped(.cdp);
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
enable,
}, cmd.action) orelse return error.UnknownMethod;
const Methods = enum {
enable,
};
pub fn dom(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
action: []const u8,
ctx: *Ctx,
) ![]const u8 {
const method = std.meta.stringToEnum(Methods, action) orelse
return error.UnknownMethod;
return switch (method) {
.enable => enable(alloc, msg, ctx),
};
}
fn enable(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "inspector.enable" });
return result(alloc, input.id, null, null, input.sessionId);
switch (action) {
.enable => return cmd.sendResult(null, .{}),
}
}

View File

@@ -17,107 +17,52 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const server = @import("../server.zig");
const Ctx = server.Ctx;
const cdp = @import("cdp.zig");
const result = cdp.result;
const stringify = cdp.stringify;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const Runtime = @import("runtime.zig");
const log = std.log.scoped(.cdp);
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
setEmulatedMedia,
setFocusEmulationEnabled,
setDeviceMetricsOverride,
setTouchEmulationEnabled,
}, cmd.action) orelse return error.UnknownMethod;
const Methods = enum {
setEmulatedMedia,
setFocusEmulationEnabled,
setDeviceMetricsOverride,
setTouchEmulationEnabled,
};
pub fn emulation(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
action: []const u8,
ctx: *Ctx,
) ![]const u8 {
const method = std.meta.stringToEnum(Methods, action) orelse
return error.UnknownMethod;
return switch (method) {
.setEmulatedMedia => setEmulatedMedia(alloc, msg, ctx),
.setFocusEmulationEnabled => setFocusEmulationEnabled(alloc, msg, ctx),
.setDeviceMetricsOverride => setDeviceMetricsOverride(alloc, msg, ctx),
.setTouchEmulationEnabled => setTouchEmulationEnabled(alloc, msg, ctx),
};
}
const MediaFeature = struct {
name: []const u8,
value: []const u8,
};
// TODO: noop method
fn setEmulatedMedia(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const Params = struct {
media: ?[]const u8 = null,
features: ?[]MediaFeature = null,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "emulation.setEmulatedMedia" });
// output
return result(alloc, input.id, null, null, input.sessionId);
switch (action) {
.setEmulatedMedia => return setEmulatedMedia(cmd),
.setFocusEmulationEnabled => return setFocusEmulationEnabled(cmd),
.setDeviceMetricsOverride => return setDeviceMetricsOverride(cmd),
.setTouchEmulationEnabled => return setTouchEmulationEnabled(cmd),
}
}
// TODO: noop method
fn setFocusEmulationEnabled(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const Params = struct {
enabled: bool,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "emulation.setFocusEmulationEnabled" });
fn setEmulatedMedia(cmd: anytype) !void {
// const input = (try const incoming.params(struct {
// media: ?[]const u8 = null,
// features: ?[]struct{
// name: []const u8,
// value: [] const u8
// } = null,
// })) orelse return error.InvalidParams;
// output
return result(alloc, input.id, null, null, input.sessionId);
return cmd.sendResult(null, .{});
}
// TODO: noop method
fn setDeviceMetricsOverride(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "emulation.setDeviceMetricsOverride" });
// output
return result(alloc, input.id, null, null, input.sessionId);
fn setFocusEmulationEnabled(cmd: anytype) !void {
// const input = (try const incoming.params(struct {
// enabled: bool,
// })) orelse return error.InvalidParams;
return cmd.sendResult(null, .{});
}
// TODO: noop method
fn setTouchEmulationEnabled(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "emulation.setTouchEmulationEnabled" });
return result(alloc, input.id, null, null, input.sessionId);
fn setDeviceMetricsOverride(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
// TODO: noop method
fn setTouchEmulationEnabled(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}

View File

@@ -17,43 +17,14 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const server = @import("../server.zig");
const Ctx = server.Ctx;
const cdp = @import("cdp.zig");
const result = cdp.result;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const log = std.log.scoped(.cdp);
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
disable,
}, cmd.action) orelse return error.UnknownMethod;
const Methods = enum {
disable,
};
pub fn fetch(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
action: []const u8,
ctx: *Ctx,
) ![]const u8 {
const method = std.meta.stringToEnum(Methods, action) orelse
return error.UnknownMethod;
return switch (method) {
.disable => disable(alloc, msg, ctx),
};
}
// TODO: noop method
fn disable(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "fetch.disable" });
return result(alloc, input.id, null, null, input.sessionId);
switch (action) {
.disable => return cmd.sendResult(null, .{}),
}
}

View File

@@ -17,43 +17,14 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const server = @import("../server.zig");
const Ctx = server.Ctx;
const cdp = @import("cdp.zig");
const result = cdp.result;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const log = std.log.scoped(.cdp);
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
enable,
}, cmd.action) orelse return error.UnknownMethod;
const Methods = enum {
enable,
};
pub fn inspector(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
action: []const u8,
ctx: *Ctx,
) ![]const u8 {
const method = std.meta.stringToEnum(Methods, action) orelse
return error.UnknownMethod;
return switch (method) {
.enable => enable(alloc, msg, ctx),
};
}
fn enable(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "inspector.enable" });
return result(alloc, input.id, null, null, input.sessionId);
switch (action) {
.enable => return cmd.sendResult(null, .{}),
}
}

View File

@@ -17,43 +17,14 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const server = @import("../server.zig");
const Ctx = server.Ctx;
const cdp = @import("cdp.zig");
const result = cdp.result;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const stringify = cdp.stringify;
const log_cdp = std.log.scoped(.cdp);
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
enable,
}, cmd.action) orelse return error.UnknownMethod;
const Methods = enum {
enable,
};
pub fn log(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
action: []const u8,
ctx: *Ctx,
) ![]const u8 {
const method = std.meta.stringToEnum(Methods, action) orelse
return error.UnknownMethod;
return switch (method) {
.enable => enable(alloc, msg, ctx),
};
}
fn enable(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log_cdp.debug("Req > id {d}, method {s}", .{ input.id, "log.enable" });
return result(alloc, input.id, null, null, input.sessionId);
switch (action) {
.enable => return cmd.sendResult(null, .{}),
}
}

View File

@@ -1,293 +0,0 @@
// 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");
// Parse incoming protocol message in json format.
pub const IncomingMessage = struct {
scanner: std.json.Scanner,
json: []const u8,
obj_begin: bool = false,
obj_end: bool = false,
id: ?u16 = null,
scan_sessionId: bool = false,
sessionId: ?[]const u8 = null,
method: ?[]const u8 = null,
params_skip: bool = false,
pub fn init(alloc: std.mem.Allocator, json: []const u8) IncomingMessage {
return .{
.json = json,
.scanner = std.json.Scanner.initCompleteInput(alloc, json),
};
}
pub fn deinit(self: *IncomingMessage) void {
self.scanner.deinit();
}
fn scanUntil(self: *IncomingMessage, key: []const u8) !void {
while (true) {
switch (try self.scanner.next()) {
.end_of_document => return error.EndOfDocument,
.object_begin => {
if (self.obj_begin) return error.InvalidObjectBegin;
self.obj_begin = true;
},
.object_end => {
if (!self.obj_begin) return error.InvalidObjectEnd;
if (self.obj_end) return error.InvalidObjectEnd;
self.obj_end = true;
},
.string => |s| {
// is the key what we expects?
if (std.mem.eql(u8, s, key)) return;
// save other known keys
if (std.mem.eql(u8, s, "id")) try self.scanId();
if (std.mem.eql(u8, s, "sessionId")) try self.scanSessionId();
if (std.mem.eql(u8, s, "method")) try self.scanMethod();
if (std.mem.eql(u8, s, "params")) try self.scanParams();
// TODO should we skip unknown key?
},
else => return error.InvalidToken,
}
}
}
fn scanId(self: *IncomingMessage) !void {
const t = try self.scanner.next();
if (t != .number) return error.InvalidId;
self.id = try std.fmt.parseUnsigned(u16, t.number, 10);
}
fn getId(self: *IncomingMessage) !u16 {
if (self.id != null) return self.id.?;
try self.scanUntil("id");
try self.scanId();
return self.id.?;
}
fn scanSessionId(self: *IncomingMessage) !void {
switch (try self.scanner.next()) {
// session id can be null.
.null => return,
.string => |s| self.sessionId = s,
else => return error.InvalidSessionId,
}
self.scan_sessionId = true;
}
fn getSessionId(self: *IncomingMessage) !?[]const u8 {
if (self.scan_sessionId) return self.sessionId;
self.scanUntil("sessionId") catch |err| {
if (err != error.EndOfDocument) return err;
// if the document doesn't contains any session id key, we must
// return null value.
self.scan_sessionId = true;
return null;
};
try self.scanSessionId();
return self.sessionId;
}
fn scanMethod(self: *IncomingMessage) !void {
const t = try self.scanner.next();
if (t != .string) return error.InvalidMethod;
self.method = t.string;
}
pub fn getMethod(self: *IncomingMessage) ![]const u8 {
if (self.method != null) return self.method.?;
try self.scanUntil("method");
try self.scanMethod();
return self.method.?;
}
// scanParams skip found parameters b/c if we encounter params *before*
// asking for getParams, we don't know how to parse them.
fn scanParams(self: *IncomingMessage) !void {
const tt = try self.scanner.peekNextTokenType();
// accept object begin or null JSON value.
if (tt != .object_begin and tt != .null) return error.InvalidParams;
try self.scanner.skipValue();
self.params_skip = true;
}
// getParams restart the JSON parsing
fn getParams(self: *IncomingMessage, alloc: ?std.mem.Allocator, T: type) !T {
if (T == void) return void{};
std.debug.assert(alloc != null); // if T is not void, alloc should not be null
if (self.params_skip) {
// TODO if the params have been skipped, we have to retart the
// parsing from start.
return error.SkippedParams;
}
self.scanUntil("params") catch |err| {
// handle nullable type
if (@typeInfo(T) == .Optional) {
if (err == error.InvalidToken or err == error.EndOfDocument) {
return null;
}
}
return err;
};
// parse "params"
const options = std.json.ParseOptions{
.ignore_unknown_fields = true,
.max_value_len = self.scanner.input.len,
.allocate = .alloc_always,
};
return try std.json.innerParse(T, alloc.?, &self.scanner, options);
}
};
pub fn Input(T: type) type {
return struct {
arena: ?*std.heap.ArenaAllocator = null,
id: u16,
params: T,
sessionId: ?[]const u8,
const Self = @This();
pub fn get(alloc: std.mem.Allocator, msg: *IncomingMessage) !Self {
var arena: ?*std.heap.ArenaAllocator = null;
var allocator: ?std.mem.Allocator = null;
if (T != void) {
arena = try alloc.create(std.heap.ArenaAllocator);
arena.?.* = std.heap.ArenaAllocator.init(alloc);
allocator = arena.?.allocator();
}
errdefer {
if (arena) |_arena| {
_arena.deinit();
alloc.destroy(_arena);
}
}
return .{
.arena = arena,
.params = try msg.getParams(allocator, T),
.id = try msg.getId(),
.sessionId = try msg.getSessionId(),
};
}
pub fn deinit(self: Self) void {
if (self.arena) |arena| {
const allocator = arena.child_allocator;
arena.deinit();
allocator.destroy(arena);
}
}
};
}
test "read incoming message" {
const inputs = [_][]const u8{
\\{"id":1,"method":"foo","sessionId":"bar","params":{"bar":"baz"}}
,
\\{"params":{"bar":"baz"},"id":1,"method":"foo","sessionId":"bar"}
,
\\{"sessionId":"bar","params":{"bar":"baz"},"id":1,"method":"foo"}
,
\\{"method":"foo","sessionId":"bar","params":{"bar":"baz"},"id":1}
,
};
for (inputs) |input| {
var msg = IncomingMessage.init(std.testing.allocator, input);
defer msg.deinit();
try std.testing.expectEqual(1, try msg.getId());
try std.testing.expectEqualSlices(u8, "foo", try msg.getMethod());
try std.testing.expectEqualSlices(u8, "bar", (try msg.getSessionId()).?);
const T = struct { bar: []const u8 };
const in = Input(T).get(std.testing.allocator, &msg) catch |err| {
if (err != error.SkippedParams) return err;
// TODO remove this check when params in the beginning is handled.
continue;
};
defer in.deinit();
try std.testing.expectEqualSlices(u8, "baz", in.params.bar);
}
}
test "read incoming message with null session id" {
const inputs = [_][]const u8{
\\{"id":1}
,
\\{"params":{"bar":"baz"},"id":1,"method":"foo"}
,
\\{"sessionId":null,"params":{"bar":"baz"},"id":1,"method":"foo"}
,
};
for (inputs) |input| {
var msg = IncomingMessage.init(std.testing.allocator, input);
defer msg.deinit();
try std.testing.expect(try msg.getSessionId() == null);
try std.testing.expectEqual(1, try msg.getId());
}
}
test "message with nullable params" {
const T = struct {
bar: []const u8,
};
// nullable type, params is present => value
const not_null =
\\{"id": 1,"method":"foo","params":{"bar":"baz"}}
;
var msg = IncomingMessage.init(std.testing.allocator, not_null);
defer msg.deinit();
const input = try Input(?T).get(std.testing.allocator, &msg);
defer input.deinit();
try std.testing.expectEqualStrings(input.params.?.bar, "baz");
// nullable type, params is not present => null
const is_null =
\\{"id": 1,"method":"foo","sessionId":"AAA"}
;
var msg_null = IncomingMessage.init(std.testing.allocator, is_null);
defer msg_null.deinit();
const input_null = try Input(?T).get(std.testing.allocator, &msg_null);
defer input_null.deinit();
try std.testing.expectEqual(null, input_null.params);
try std.testing.expectEqualStrings("AAA", input_null.sessionId.?);
// not nullable type, params is not present => error
const params_or_error = msg_null.getParams(std.testing.allocator, T);
try std.testing.expectError(error.EndOfDocument, params_or_error);
}

View File

@@ -17,59 +17,16 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const server = @import("../server.zig");
const Ctx = server.Ctx;
const cdp = @import("cdp.zig");
const result = cdp.result;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const log = std.log.scoped(.cdp);
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
enable,
setCacheDisabled,
}, cmd.action) orelse return error.UnknownMethod;
const Methods = enum {
enable,
setCacheDisabled,
};
pub fn network(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
action: []const u8,
ctx: *Ctx,
) ![]const u8 {
const method = std.meta.stringToEnum(Methods, action) orelse
return error.UnknownMethod;
return switch (method) {
.enable => enable(alloc, msg, ctx),
.setCacheDisabled => setCacheDisabled(alloc, msg, ctx),
};
}
fn enable(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "network.enable" });
return result(alloc, input.id, null, null, input.sessionId);
}
// TODO: noop method
fn setCacheDisabled(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "network.setCacheDisabled" });
return result(alloc, input.id, null, null, input.sessionId);
switch (action) {
.enable => return cmd.sendResult(null, .{}),
.setCacheDisabled => return cmd.sendResult(null, .{}),
}
}

View File

@@ -17,58 +17,27 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const server = @import("../server.zig");
const Ctx = server.Ctx;
const cdp = @import("cdp.zig");
const result = cdp.result;
const stringify = cdp.stringify;
const sendEvent = cdp.sendEvent;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const runtime = @import("runtime.zig");
const log = std.log.scoped(.cdp);
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
enable,
getFrameTree,
setLifecycleEventsEnabled,
addScriptToEvaluateOnNewDocument,
createIsolatedWorld,
navigate,
}, cmd.action) orelse return error.UnknownMethod;
const Runtime = @import("runtime.zig");
const Methods = enum {
enable,
getFrameTree,
setLifecycleEventsEnabled,
addScriptToEvaluateOnNewDocument,
createIsolatedWorld,
navigate,
};
pub fn page(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
action: []const u8,
ctx: *Ctx,
) ![]const u8 {
const method = std.meta.stringToEnum(Methods, action) orelse
return error.UnknownMethod;
return switch (method) {
.enable => enable(alloc, msg, ctx),
.getFrameTree => getFrameTree(alloc, msg, ctx),
.setLifecycleEventsEnabled => setLifecycleEventsEnabled(alloc, msg, ctx),
.addScriptToEvaluateOnNewDocument => addScriptToEvaluateOnNewDocument(alloc, msg, ctx),
.createIsolatedWorld => createIsolatedWorld(alloc, msg, ctx),
.navigate => navigate(alloc, msg, ctx),
};
}
fn enable(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "page.enable" });
return result(alloc, input.id, null, null, input.sessionId);
switch (action) {
.enable => return cmd.sendResult(null, .{}),
.getFrameTree => return getFrameTree(cmd),
.setLifecycleEventsEnabled => return setLifecycleEventsEnabled(cmd),
.addScriptToEvaluateOnNewDocument => return addScriptToEvaluateOnNewDocument(cmd),
.createIsolatedWorld => return createIsolatedWorld(cmd),
.navigate => return navigate(cmd),
}
}
const Frame = struct {
@@ -86,16 +55,7 @@ const Frame = struct {
gatedAPIFeatures: [][]const u8 = &[0][]const u8{},
};
fn getFrameTree(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "page.getFrameTree" });
fn getFrameTree(cmd: anytype) !void {
// output
const FrameTree = struct {
frameTree: struct {
@@ -112,6 +72,7 @@ fn getFrameTree(
try writer.writeAll("cdp.page.getFrameTree { ");
try writer.writeAll(".frameTree = { ");
try writer.writeAll(".frame = { ");
const frame = self.frameTree.frame;
try writer.writeAll(".id = ");
try std.fmt.formatText(frame.id, "s", options, writer);
@@ -122,65 +83,40 @@ fn getFrameTree(
try writer.writeAll(" } } }");
}
};
const frameTree = FrameTree{
const state = cmd.cdp;
return cmd.sendResult(FrameTree{
.frameTree = .{
.frame = .{
.id = ctx.state.frameID,
.url = ctx.state.url,
.securityOrigin = ctx.state.securityOrigin,
.secureContextType = ctx.state.secureContextType,
.loaderId = ctx.state.loaderID,
.id = state.frame_id,
.url = state.url,
.securityOrigin = state.security_origin,
.secureContextType = state.secure_context_type,
.loaderId = state.loader_id,
},
},
};
return result(alloc, input.id, FrameTree, frameTree, input.sessionId);
}, .{});
}
fn setLifecycleEventsEnabled(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const Params = struct {
enabled: bool,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "page.setLifecycleEventsEnabled" });
fn setLifecycleEventsEnabled(cmd: anytype) !void {
// const params = (try cmd.params(struct {
// enabled: bool,
// })) orelse return error.InvalidParams;
ctx.state.page_life_cycle_events = true;
// output
return result(alloc, input.id, null, null, input.sessionId);
cmd.cdp.page_life_cycle_events = true;
return cmd.sendResult(null, .{});
}
const LifecycleEvent = struct {
frameId: []const u8,
loaderId: ?[]const u8,
name: []const u8 = undefined,
timestamp: f32 = undefined,
};
// TODO: hard coded method
fn addScriptToEvaluateOnNewDocument(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const Params = struct {
source: []const u8,
worldName: ?[]const u8 = null,
includeCommandLineAPI: bool = false,
runImmediately: bool = false,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "page.addScriptToEvaluateOnNewDocument" });
fn addScriptToEvaluateOnNewDocument(cmd: anytype) !void {
// const params = (try cmd.params(struct {
// source: []const u8,
// worldName: ?[]const u8 = null,
// includeCommandLineAPI: bool = false,
// runImmediately: bool = false,
// })) orelse return error.InvalidParams;
// output
const Res = struct {
const Response = struct {
identifier: []const u8 = "1",
pub fn format(
@@ -195,109 +131,84 @@ fn addScriptToEvaluateOnNewDocument(
try writer.writeAll(" }");
}
};
return result(alloc, input.id, Res, Res{}, input.sessionId);
return cmd.sendResult(Response{}, .{});
}
// TODO: hard coded method
fn createIsolatedWorld(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const Params = struct {
fn createIsolatedWorld(cmd: anytype) !void {
const session_id = cmd.session_id orelse return error.SessionIdRequired;
const params = (try cmd.params(struct {
frameId: []const u8,
worldName: []const u8,
grantUniveralAccess: bool,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
std.debug.assert(input.sessionId != null);
log.debug("Req > id {d}, method {s}", .{ input.id, "page.createIsolatedWorld" });
})) orelse return error.InvalidParams;
// noop executionContextCreated event
try Runtime.executionContextCreated(
alloc,
ctx,
0,
"",
input.params.worldName,
// TODO: hard coded ID
"7102379147004877974.3265385113993241162",
.{
.isDefault = false,
.type = "isolated",
.frameId = input.params.frameId,
try cmd.sendEvent("Runtime.executionContextCreated", .{
.context = runtime.ExecutionContextCreated{
.id = 0,
.origin = "",
.name = params.worldName,
// TODO: hard coded ID
.uniqueId = "7102379147004877974.3265385113993241162",
.auxData = .{
.isDefault = false,
.type = "isolated",
.frameId = params.frameId,
},
},
input.sessionId,
);
}, .{ .session_id = session_id });
// output
const Resp = struct {
executionContextId: u8 = 0,
};
return result(alloc, input.id, Resp, .{}, input.sessionId);
return cmd.sendResult(.{
.executionContextId = 0,
}, .{});
}
fn navigate(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const Params = struct {
fn navigate(cmd: anytype) !void {
const session_id = cmd.session_id orelse return error.SessionIdRequired;
const params = (try cmd.params(struct {
url: []const u8,
referrer: ?[]const u8 = null,
transitionType: ?[]const u8 = null, // TODO: enum
frameId: ?[]const u8 = null,
referrerPolicy: ?[]const u8 = null, // TODO: enum
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
std.debug.assert(input.sessionId != null);
log.debug("Req > id {d}, method {s}", .{ input.id, "page.navigate" });
})) orelse return error.InvalidParams;
// change state
ctx.state.url = input.params.url;
var state = cmd.cdp;
state.url = params.url;
// TODO: hard coded ID
ctx.state.loaderID = "AF8667A203C5392DBE9AC290044AA4C2";
state.loader_id = "AF8667A203C5392DBE9AC290044AA4C2";
const LifecycleEvent = struct {
frameId: []const u8,
loaderId: ?[]const u8,
name: []const u8,
timestamp: f32,
};
var life_event = LifecycleEvent{
.frameId = ctx.state.frameID,
.loaderId = ctx.state.loaderID,
.frameId = state.frame_id,
.loaderId = state.loader_id,
.name = "init",
.timestamp = 343721.796037,
};
var ts_event: cdp.TimestampEvent = undefined;
// frameStartedLoading event
// TODO: event partially hard coded
const FrameStartedLoading = struct {
frameId: []const u8,
};
const frame_started_loading = FrameStartedLoading{ .frameId = ctx.state.frameID };
try sendEvent(
alloc,
ctx,
"Page.frameStartedLoading",
FrameStartedLoading,
frame_started_loading,
input.sessionId,
);
if (ctx.state.page_life_cycle_events) {
life_event.name = "init";
life_event.timestamp = 343721.796037;
try sendEvent(
alloc,
ctx,
"Page.lifecycleEvent",
LifecycleEvent,
life_event,
input.sessionId,
);
try cmd.sendEvent("Page.frameStartedLoading", .{
.frameId = state.frame_id,
}, .{ .session_id = session_id });
if (state.page_life_cycle_events) {
try cmd.sendEvent("Page.lifecycleEvent", life_event, .{ .session_id = session_id });
}
// output
const Resp = struct {
const Response = struct {
frameId: []const u8,
loaderId: ?[]const u8,
errorText: ?[]const u8 = null,
@@ -318,136 +229,87 @@ fn navigate(
try writer.writeAll(" }");
}
};
const resp = Resp{
.frameId = ctx.state.frameID,
.loaderId = ctx.state.loaderID,
};
const res = try result(alloc, input.id, Resp, resp, input.sessionId);
try ctx.send(res);
try cmd.sendResult(Response{
.frameId = state.frame_id,
.loaderId = state.loader_id,
}, .{});
// TODO: at this point do we need async the following actions to be async?
// Send Runtime.executionContextsCleared event
// TODO: noop event, we have no env context at this point, is it necesarry?
try sendEvent(alloc, ctx, "Runtime.executionContextsCleared", struct {}, .{}, input.sessionId);
try cmd.sendEvent("Runtime.executionContextsCleared", null, .{ .session_id = session_id });
// Launch navigate, the page must have been created by a
// target.createTarget.
var p = ctx.browser.session.page orelse return error.NoPage;
ctx.state.executionContextId += 1;
const auxData = try std.fmt.allocPrint(
alloc,
var p = cmd.session.page orelse return error.NoPage;
state.execution_context_id += 1;
const aux_data = try std.fmt.allocPrint(
cmd.arena,
// NOTE: we assume this is the default web page
"{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}",
.{ctx.state.frameID},
.{state.frame_id},
);
defer alloc.free(auxData);
try p.navigate(input.params.url, auxData);
try p.navigate(params.url, aux_data);
// Events
// lifecycle init event
// TODO: partially hard coded
if (ctx.state.page_life_cycle_events) {
if (state.page_life_cycle_events) {
life_event.name = "init";
life_event.timestamp = 343721.796037;
try sendEvent(
alloc,
ctx,
"Page.lifecycleEvent",
LifecycleEvent,
life_event,
input.sessionId,
);
try cmd.sendEvent("Page.lifecycleEvent", life_event, .{ .session_id = session_id });
}
// frameNavigated event
const FrameNavigated = struct {
frame: Frame,
type: []const u8 = "Navigation",
};
const frame_navigated = FrameNavigated{
.frame = .{
.id = ctx.state.frameID,
.url = ctx.state.url,
.securityOrigin = ctx.state.securityOrigin,
.secureContextType = ctx.state.secureContextType,
.loaderId = ctx.state.loaderID,
try cmd.sendEvent("Page.frameNavigated", .{
.type = "Navigation",
.frame = Frame{
.id = state.frame_id,
.url = state.url,
.securityOrigin = state.security_origin,
.secureContextType = state.secure_context_type,
.loaderId = state.loader_id,
},
};
try sendEvent(
alloc,
ctx,
"Page.frameNavigated",
FrameNavigated,
frame_navigated,
input.sessionId,
);
}, .{ .session_id = session_id });
// domContentEventFired event
// TODO: partially hard coded
ts_event = .{ .timestamp = 343721.803338 };
try sendEvent(
alloc,
ctx,
try cmd.sendEvent(
"Page.domContentEventFired",
cdp.TimestampEvent,
ts_event,
input.sessionId,
cdp.TimestampEvent{ .timestamp = 343721.803338 },
.{ .session_id = session_id },
);
// lifecycle DOMContentLoaded event
// TODO: partially hard coded
if (ctx.state.page_life_cycle_events) {
if (state.page_life_cycle_events) {
life_event.name = "DOMContentLoaded";
life_event.timestamp = 343721.803338;
try sendEvent(
alloc,
ctx,
"Page.lifecycleEvent",
LifecycleEvent,
life_event,
input.sessionId,
);
try cmd.sendEvent("Page.lifecycleEvent", life_event, .{ .session_id = session_id });
}
// loadEventFired event
// TODO: partially hard coded
ts_event = .{ .timestamp = 343721.824655 };
try sendEvent(
alloc,
ctx,
try cmd.sendEvent(
"Page.loadEventFired",
cdp.TimestampEvent,
ts_event,
input.sessionId,
cdp.TimestampEvent{ .timestamp = 343721.824655 },
.{ .session_id = session_id },
);
// lifecycle DOMContentLoaded event
// TODO: partially hard coded
if (ctx.state.page_life_cycle_events) {
if (state.page_life_cycle_events) {
life_event.name = "load";
life_event.timestamp = 343721.824655;
try sendEvent(
alloc,
ctx,
"Page.lifecycleEvent",
LifecycleEvent,
life_event,
input.sessionId,
);
try cmd.sendEvent("Page.lifecycleEvent", life_event, .{ .session_id = session_id });
}
// frameStoppedLoading
const FrameStoppedLoading = struct { frameId: []const u8 };
try sendEvent(
alloc,
ctx,
"Page.frameStoppedLoading",
FrameStoppedLoading,
.{ .frameId = ctx.state.frameID },
input.sessionId,
);
return "";
return cmd.sendEvent("Page.frameStoppedLoading", .{
.frameId = state.frame_id,
}, .{ .session_id = session_id });
}

View File

@@ -17,43 +17,15 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const server = @import("../server.zig");
const Ctx = server.Ctx;
const cdp = @import("cdp.zig");
const result = cdp.result;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const asUint = @import("../str/parser.zig").asUint;
const log = std.log.scoped(.cdp);
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
enable,
}, cmd.action) orelse return error.UnknownMethod;
const Methods = enum {
enable,
};
pub fn performance(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
action: []const u8,
ctx: *Ctx,
) ![]const u8 {
const method = std.meta.stringToEnum(Methods, action) orelse
return error.UnknownMethod;
return switch (method) {
.enable => enable(alloc, msg, ctx),
};
}
fn enable(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "performance.enable" });
return result(alloc, input.id, null, null, input.sessionId);
switch (action) {
.enable => return cmd.sendResult(null, .{}),
}
}

View File

@@ -17,179 +17,106 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const builtin = @import("builtin");
const jsruntime = @import("jsruntime");
const server = @import("../server.zig");
const Ctx = server.Ctx;
const cdp = @import("cdp.zig");
const result = cdp.result;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const stringify = cdp.stringify;
const target = @import("target.zig");
const log = std.log.scoped(.cdp);
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
enable,
runIfWaitingForDebugger,
evaluate,
addBinding,
callFunctionOn,
releaseObject,
}, cmd.action) orelse return error.UnknownMethod;
const Methods = enum {
enable,
runIfWaitingForDebugger,
evaluate,
addBinding,
callFunctionOn,
releaseObject,
};
pub fn runtime(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
action: []const u8,
ctx: *Ctx,
) ![]const u8 {
const method = std.meta.stringToEnum(Methods, action) orelse
// NOTE: we could send it anyway to the JS runtime but it's good to check it
return error.UnknownMethod;
return switch (method) {
.runIfWaitingForDebugger => runIfWaitingForDebugger(alloc, msg, ctx),
else => sendInspector(alloc, method, msg, ctx),
};
switch (action) {
.runIfWaitingForDebugger => return cmd.sendResult(null, .{}),
else => return sendInspector(cmd, action),
}
}
fn sendInspector(
alloc: std.mem.Allocator,
method: Methods,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
fn sendInspector(cmd: anytype, action: anytype) !void {
// save script in file at debug mode
if (std.log.defaultLogEnabled(.debug)) {
// input
var id: u16 = undefined;
var script: ?[]const u8 = null;
if (method == .evaluate) {
const Params = struct {
expression: []const u8,
contextId: ?u8 = null,
returnByValue: ?bool = null,
awaitPromise: ?bool = null,
userGesture: ?bool = null,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s} (script saved on cache)", .{ input.id, "runtime.evaluate" });
const params = input.params;
const func = try alloc.alloc(u8, params.expression.len);
@memcpy(func, params.expression);
script = func;
id = input.id;
} else if (method == .callFunctionOn) {
const 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,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s} (script saved on cache)", .{ input.id, "runtime.callFunctionOn" });
const params = input.params;
const func = try alloc.alloc(u8, params.functionDeclaration.len);
@memcpy(func, params.functionDeclaration);
script = func;
id = input.id;
}
if (script) |src| {
try cdp.dumpFile(alloc, id, src);
alloc.free(src);
}
try logInspector(cmd, action);
}
if (msg.sessionId) |s| {
ctx.state.sessionID = cdp.SessionID.parse(s) catch |err| {
log.err("parse sessionID: {s} {any}", .{ s, err });
return err;
};
if (cmd.session_id) |s| {
cmd.cdp.session_id = try cdp.SessionID.parse(s);
}
// remove awaitPromise true params
// TODO: delete when Promise are correctly handled by zig-js-runtime
if (method == .callFunctionOn or method == .evaluate) {
if (std.mem.indexOf(u8, msg.json, "\"awaitPromise\":true")) |_| {
const buf = try alloc.alloc(u8, msg.json.len + 1);
defer alloc.free(buf);
_ = std.mem.replace(u8, msg.json, "\"awaitPromise\":true", "\"awaitPromise\":false", buf);
try ctx.sendInspector(buf);
return "";
if (action == .callFunctionOn or action == .evaluate) {
const json = cmd.json;
if (std.mem.indexOf(u8, json, "\"awaitPromise\":true")) |_| {
// +1 because we'll be turning a true -> false
const buf = try cmd.arena.alloc(u8, json.len + 1);
_ = std.mem.replace(u8, json, "\"awaitPromise\":true", "\"awaitPromise\":false", buf);
cmd.session.callInspector(buf);
return;
}
}
try ctx.sendInspector(msg.json);
cmd.session.callInspector(cmd.json);
if (msg.id == null) return "";
return result(alloc, msg.id.?, null, null, msg.sessionId);
if (cmd.id != null) {
return cmd.sendResult(null, .{});
}
}
pub const AuxData = struct {
isDefault: bool = true,
type: []const u8 = "default",
frameId: []const u8 = cdp.FrameID,
};
pub fn executionContextCreated(
alloc: std.mem.Allocator,
ctx: *Ctx,
id: u16,
pub const ExecutionContextCreated = struct {
id: u64,
origin: []const u8,
name: []const u8,
uniqueID: []const u8,
auxData: ?AuxData,
sessionID: ?[]const u8,
) !void {
const Params = struct {
context: struct {
id: u64,
origin: []const u8,
name: []const u8,
uniqueId: []const u8,
auxData: ?AuxData = null,
},
};
const params = Params{
.context = .{
.id = id,
.origin = origin,
.name = name,
.uniqueId = uniqueID,
.auxData = auxData,
},
};
try cdp.sendEvent(alloc, ctx, "Runtime.executionContextCreated", Params, params, sessionID);
}
uniqueId: []const u8,
auxData: ?AuxData = null,
// TODO: noop method
// should we be passing this also to the JS Inspector?
fn runIfWaitingForDebugger(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "runtime.runIfWaitingForDebugger" });
pub const AuxData = struct {
isDefault: bool = true,
type: []const u8 = "default",
frameId: []const u8 = cdp.FRAME_ID,
};
};
return result(alloc, input.id, null, null, input.sessionId);
fn logInspector(cmd: anytype, 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.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);
}

View File

@@ -17,43 +17,14 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const server = @import("../server.zig");
const Ctx = server.Ctx;
const cdp = @import("cdp.zig");
const result = cdp.result;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const log = std.log.scoped(.cdp);
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
enable,
}, cmd.action) orelse return error.UnknownMethod;
const Methods = enum {
enable,
};
pub fn security(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
action: []const u8,
ctx: *Ctx,
) ![]const u8 {
const method = std.meta.stringToEnum(Methods, action) orelse
return error.UnknownMethod;
return switch (method) {
.enable => enable(alloc, msg, ctx),
};
}
fn enable(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "security.enable" });
return result(alloc, input.id, null, null, input.sessionId);
switch (action) {
.enable => return cmd.sendResult(null, .{}),
}
}

View File

@@ -17,267 +17,174 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const server = @import("../server.zig");
const Ctx = server.Ctx;
const cdp = @import("cdp.zig");
const result = cdp.result;
const stringify = cdp.stringify;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const log = std.log.scoped(.cdp);
const Methods = enum {
setDiscoverTargets,
setAutoAttach,
attachToTarget,
getTargetInfo,
getBrowserContexts,
createBrowserContext,
disposeBrowserContext,
createTarget,
closeTarget,
sendMessageToTarget,
detachFromTarget,
};
pub fn target(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
action: []const u8,
ctx: *Ctx,
) ![]const u8 {
const method = std.meta.stringToEnum(Methods, action) orelse
return error.UnknownMethod;
return switch (method) {
.setDiscoverTargets => setDiscoverTargets(alloc, msg, ctx),
.setAutoAttach => setAutoAttach(alloc, msg, ctx),
.attachToTarget => attachToTarget(alloc, msg, ctx),
.getTargetInfo => getTargetInfo(alloc, msg, ctx),
.getBrowserContexts => getBrowserContexts(alloc, msg, ctx),
.createBrowserContext => createBrowserContext(alloc, msg, ctx),
.disposeBrowserContext => disposeBrowserContext(alloc, msg, ctx),
.createTarget => createTarget(alloc, msg, ctx),
.closeTarget => closeTarget(alloc, msg, ctx),
.sendMessageToTarget => sendMessageToTarget(alloc, msg, ctx),
.detachFromTarget => detachFromTarget(alloc, msg, ctx),
};
}
// TODO: hard coded IDs
pub const PageTargetID = "PAGETARGETIDB638E9DC0F52DDC";
pub const BrowserTargetID = "browser9-targ-et6f-id0e-83f3ab73a30c";
pub const BrowserContextID = "BROWSERCONTEXTIDA95049E9DFE95EA9";
const CONTEXT_ID = "CONTEXTIDDCCDD11109E2D4FEFBE4F89";
const PAGE_TARGET_ID = "PAGETARGETIDB638E9DC0F52DDC";
const BROWSER_TARGET_ID = "browser9-targ-et6f-id0e-83f3ab73a30c";
const BROWER_CONTEXT_ID = "BROWSERCONTEXTIDA95049E9DFE95EA9";
const TARGET_ID = "TARGETID460A8F29706A2ADF14316298";
const LOADER_ID = "LOADERID42AA389647D702B4D805F49A";
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
setDiscoverTargets,
setAutoAttach,
attachToTarget,
getTargetInfo,
getBrowserContexts,
createBrowserContext,
disposeBrowserContext,
createTarget,
closeTarget,
sendMessageToTarget,
detachFromTarget,
}, cmd.action) orelse return error.UnknownMethod;
switch (action) {
.setDiscoverTargets => return setDiscoverTargets(cmd),
.setAutoAttach => return setAutoAttach(cmd),
.attachToTarget => return attachToTarget(cmd),
.getTargetInfo => return getTargetInfo(cmd),
.getBrowserContexts => return getBrowserContexts(cmd),
.createBrowserContext => return createBrowserContext(cmd),
.disposeBrowserContext => return disposeBrowserContext(cmd),
.createTarget => return createTarget(cmd),
.closeTarget => return closeTarget(cmd),
.sendMessageToTarget => return sendMessageToTarget(cmd),
.detachFromTarget => return detachFromTarget(cmd),
}
}
// TODO: noop method
fn setDiscoverTargets(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "target.setDiscoverTargets" });
// output
return result(alloc, input.id, null, null, input.sessionId);
fn setDiscoverTargets(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
const AttachToTarget = struct {
sessionId: []const u8,
targetInfo: struct {
targetId: []const u8,
type: []const u8 = "page",
title: []const u8,
url: []const u8,
attached: bool = true,
canAccessOpener: bool = false,
browserContextId: []const u8,
},
targetInfo: TargetInfo,
waitingForDebugger: bool = false,
};
const TargetCreated = struct {
sessionId: []const u8,
targetInfo: struct {
targetId: []const u8,
type: []const u8 = "page",
title: []const u8,
url: []const u8,
attached: bool = true,
canAccessOpener: bool = false,
browserContextId: []const u8,
},
targetInfo: TargetInfo,
};
const TargetFilter = struct {
type: ?[]const u8 = null,
exclude: ?bool = null,
const TargetInfo = struct {
targetId: []const u8,
type: []const u8 = "page",
title: []const u8,
url: []const u8,
attached: bool = true,
canAccessOpener: bool = false,
browserContextId: []const u8,
};
// TODO: noop method
fn setAutoAttach(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const Params = struct {
autoAttach: bool,
waitForDebuggerOnStart: bool,
flatten: bool = true,
filter: ?[]TargetFilter = null,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "target.setAutoAttach" });
fn setAutoAttach(cmd: anytype) !void {
// const TargetFilter = struct {
// type: ?[]const u8 = null,
// exclude: ?bool = null,
// };
// const params = (try cmd.params(struct {
// autoAttach: bool,
// waitForDebuggerOnStart: bool,
// flatten: bool = true,
// filter: ?[]TargetFilter = null,
// })) orelse return error.InvalidParams;
// attachedToTarget event
if (input.sessionId == null) {
const attached = AttachToTarget{
.sessionId = cdp.BrowserSessionID,
if (cmd.session_id == null) {
try cmd.sendEvent("Target.attachedToTarget", AttachToTarget{
.sessionId = cdp.BROWSER_SESSION_ID,
.targetInfo = .{
.targetId = PageTargetID,
.targetId = PAGE_TARGET_ID,
.title = "about:blank",
.url = cdp.URLBase,
.browserContextId = BrowserContextID,
.url = cdp.URL_BASE,
.browserContextId = BROWER_CONTEXT_ID,
},
};
try cdp.sendEvent(alloc, ctx, "Target.attachedToTarget", AttachToTarget, attached, null);
}, .{});
}
// output
return result(alloc, input.id, null, null, input.sessionId);
return cmd.sendResult(null, .{});
}
// TODO: noop method
fn attachToTarget(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const Params = struct {
fn attachToTarget(cmd: anytype) !void {
const params = (try cmd.params(struct {
targetId: []const u8,
flatten: bool = true,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "target.attachToTarget" });
})) orelse return error.InvalidParams;
// attachedToTarget event
if (input.sessionId == null) {
const attached = AttachToTarget{
.sessionId = cdp.BrowserSessionID,
if (cmd.session_id == null) {
try cmd.sendEvent("Target.attachedToTarget", AttachToTarget{
.sessionId = cdp.BROWSER_SESSION_ID,
.targetInfo = .{
.targetId = input.params.targetId,
.targetId = params.targetId,
.title = "about:blank",
.url = cdp.URLBase,
.browserContextId = BrowserContextID,
.url = cdp.URL_BASE,
.browserContextId = BROWER_CONTEXT_ID,
},
};
try cdp.sendEvent(alloc, ctx, "Target.attachedToTarget", AttachToTarget, attached, null);
}, .{});
}
// output
const SessionId = struct {
sessionId: []const u8,
};
const output = SessionId{
.sessionId = input.sessionId orelse cdp.BrowserSessionID,
};
return result(alloc, input.id, SessionId, output, null);
return cmd.sendResult(
.{ .sessionId = cmd.session_id orelse cdp.BROWSER_SESSION_ID },
.{ .include_session_id = false },
);
}
fn getTargetInfo(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const Params = struct {
targetId: ?[]const u8 = null,
};
const input = try Input(?Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "target.getTargetInfo" });
fn getTargetInfo(cmd: anytype) !void {
// const params = (try cmd.params(struct {
// targetId: ?[]const u8 = null,
// })) orelse return error.InvalidParams;
// output
const TargetInfo = struct {
targetId: []const u8,
type: []const u8,
title: []const u8 = "",
url: []const u8 = "",
attached: bool = true,
openerId: ?[]const u8 = null,
canAccessOpener: bool = false,
openerFrameId: ?[]const u8 = null,
browserContextId: ?[]const u8 = null,
subtype: ?[]const u8 = null,
};
const targetInfo = TargetInfo{
.targetId = BrowserTargetID,
return cmd.sendResult(.{
.targetId = BROWSER_TARGET_ID,
.type = "browser",
};
return result(alloc, input.id, TargetInfo, targetInfo, null);
.title = "",
.url = "",
.attached = true,
.canAccessOpener = false,
}, .{ .include_session_id = false });
}
// Browser context are not handled and not in the roadmap for now
// The following methods are "fake"
// TODO: noop method
fn getBrowserContexts(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "target.getBrowserContexts" });
// ouptut
const Resp = struct {
browserContextIds: [][]const u8,
};
var resp: Resp = undefined;
if (ctx.state.contextID) |contextID| {
var contextIDs = [1][]const u8{contextID};
resp = .{ .browserContextIds = &contextIDs };
fn getBrowserContexts(cmd: anytype) !void {
var context_ids: []const []const u8 = undefined;
if (cmd.cdp.context_id) |context_id| {
context_ids = &.{context_id};
} else {
const contextIDs = [0][]const u8{};
resp = .{ .browserContextIds = &contextIDs };
context_ids = &.{};
}
return result(alloc, input.id, Resp, resp, null);
return cmd.sendResult(.{
.browserContextIds = context_ids,
}, .{ .include_session_id = false });
}
const ContextID = "CONTEXTIDDCCDD11109E2D4FEFBE4F89";
// TODO: noop method
fn createBrowserContext(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const Params = struct {
disposeOnDetach: bool = false,
proxyServer: ?[]const u8 = null,
proxyBypassList: ?[]const u8 = null,
originsWithUniversalNetworkAccess: ?[][]const u8 = null,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "target.createBrowserContext" });
fn createBrowserContext(cmd: anytype) !void {
// const params = (try cmd.params(struct {
// disposeOnDetach: bool = false,
// proxyServer: ?[]const u8 = null,
// proxyBypassList: ?[]const u8 = null,
// originsWithUniversalNetworkAccess: ?[][]const u8 = null,
// })) orelse return error.InvalidParams;
ctx.state.contextID = ContextID;
cmd.cdp.context_id = CONTEXT_ID;
// output
const Resp = struct {
browserContextId: []const u8 = ContextID,
const Response = struct {
browserContextId: []const u8,
pub fn format(
self: @This(),
@@ -291,40 +198,26 @@ fn createBrowserContext(
try writer.writeAll(" }");
}
};
return result(alloc, input.id, Resp, Resp{}, input.sessionId);
return cmd.sendResult(Response{
.browserContextId = CONTEXT_ID,
}, .{});
}
fn disposeBrowserContext(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const Params = struct {
browserContextId: []const u8,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "target.disposeBrowserContext" });
fn disposeBrowserContext(cmd: anytype) !void {
// const params = (try cmd.params(struct {
// browserContextId: []const u8,
// proxyServer: ?[]const u8 = null,
// proxyBypassList: ?[]const u8 = null,
// originsWithUniversalNetworkAccess: ?[][]const u8 = null,
// })) orelse return error.InvalidParams;
// output
const res = try result(alloc, input.id, null, .{}, null);
try ctx.send(res);
return error.DisposeBrowserContext;
try cmd.cdp.newSession();
try cmd.sendResult(null, .{});
}
// TODO: hard coded IDs
const TargetID = "TARGETID460A8F29706A2ADF14316298";
const LoaderID = "LOADERID42AA389647D702B4D805F49A";
fn createTarget(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const Params = struct {
fn createTarget(cmd: anytype) !void {
const params = (try cmd.params(struct {
url: []const u8,
width: ?u64 = null,
height: ?u64 = null,
@@ -333,71 +226,67 @@ fn createTarget(
newWindow: bool = false,
background: bool = false,
forTab: ?bool = null,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "target.createTarget" });
})) orelse return error.InvalidParams;
// change CDP state
ctx.state.frameID = TargetID;
ctx.state.url = "about:blank";
ctx.state.securityOrigin = "://";
ctx.state.secureContextType = "InsecureScheme";
ctx.state.loaderID = LoaderID;
var state = cmd.cdp;
state.frame_id = TARGET_ID;
state.url = "about:blank";
state.security_origin = "://";
state.secure_context_type = "InsecureScheme";
state.loader_id = LOADER_ID;
if (msg.sessionId) |s| {
ctx.state.sessionID = cdp.SessionID.parse(s) catch |err| {
log.err("parse sessionID: {s} {any}", .{ s, err });
return err;
};
if (cmd.session_id) |s| {
state.session_id = try cdp.SessionID.parse(s);
}
// TODO stop the previous page instead?
if (ctx.browser.session.page != null) return error.pageAlreadyExists;
if (cmd.session.page != null) {
return error.pageAlreadyExists;
}
// create the page
const p = try ctx.browser.session.createPage();
ctx.state.executionContextId += 1;
const p = try cmd.session.createPage();
state.execution_context_id += 1;
// start the js env
const auxData = try std.fmt.allocPrint(
alloc,
const aux_data = try std.fmt.allocPrint(
cmd.arena,
// NOTE: we assume this is the default web page
"{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}",
.{ctx.state.frameID},
.{state.frame_id},
);
defer alloc.free(auxData);
try p.start(auxData);
try p.start(aux_data);
const browser_context_id = params.browserContextId orelse CONTEXT_ID;
// send targetCreated event
const created = TargetCreated{
.sessionId = cdp.ContextSessionID,
try cmd.sendEvent("Target.targetCreated", TargetCreated{
.sessionId = cdp.CONTEXT_SESSION_ID,
.targetInfo = .{
.targetId = ctx.state.frameID,
.targetId = state.frame_id,
.title = "about:blank",
.url = ctx.state.url,
.browserContextId = input.params.browserContextId orelse ContextID,
.url = state.url,
.browserContextId = browser_context_id,
.attached = true,
},
};
try cdp.sendEvent(alloc, ctx, "Target.targetCreated", TargetCreated, created, input.sessionId);
}, .{ .session_id = cmd.session_id });
// send attachToTarget event
const attached = AttachToTarget{
.sessionId = cdp.ContextSessionID,
try cmd.sendEvent("Target.attachedToTarget", AttachToTarget{
.sessionId = cdp.CONTEXT_SESSION_ID,
.waitingForDebugger = true,
.targetInfo = .{
.targetId = ctx.state.frameID,
.targetId = state.frame_id,
.title = "about:blank",
.url = ctx.state.url,
.browserContextId = input.params.browserContextId orelse ContextID,
.url = state.url,
.browserContextId = browser_context_id,
.attached = true,
},
.waitingForDebugger = true,
};
try cdp.sendEvent(alloc, ctx, "Target.attachedToTarget", AttachToTarget, attached, input.sessionId);
}, .{ .session_id = cmd.session_id });
// output
const Resp = struct {
targetId: []const u8 = TargetID,
const Response = struct {
targetId: []const u8 = TARGET_ID,
pub fn format(
self: @This(),
@@ -411,119 +300,71 @@ fn createTarget(
try writer.writeAll(" }");
}
};
return result(alloc, input.id, Resp, Resp{}, input.sessionId);
return cmd.sendResult(Response{}, .{});
}
fn closeTarget(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const Params = struct {
fn closeTarget(cmd: anytype) !void {
const params = (try cmd.params(struct {
targetId: []const u8,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "target.closeTarget" });
})) orelse return error.InvalidParams;
// output
const Resp = struct {
success: bool = true,
};
const res = try result(alloc, input.id, Resp, Resp{}, null);
try ctx.send(res);
try cmd.sendResult(.{
.success = true,
}, .{ .include_session_id = false });
const session_id = cmd.session_id orelse cdp.CONTEXT_SESSION_ID;
// Inspector.detached event
const InspectorDetached = struct {
reason: []const u8 = "Render process gone.",
};
try cdp.sendEvent(
alloc,
ctx,
"Inspector.detached",
InspectorDetached,
.{},
input.sessionId orelse cdp.ContextSessionID,
);
try cmd.sendEvent("Inspector.detached", .{
.reason = "Render process gone.",
}, .{ .session_id = session_id });
// detachedFromTarget event
const TargetDetached = struct {
sessionId: []const u8,
targetId: []const u8,
};
try cdp.sendEvent(
alloc,
ctx,
"Target.detachedFromTarget",
TargetDetached,
.{
.sessionId = input.sessionId orelse cdp.ContextSessionID,
.targetId = input.params.targetId,
},
null,
);
try cmd.sendEvent("Target.detachedFromTarget", .{
.sessionId = session_id,
.targetId = params.targetId,
.reason = "Render process gone.",
}, .{});
if (ctx.browser.session.page != null) ctx.browser.session.page.?.end();
return "";
if (cmd.session.page) |*page| {
page.end();
}
}
fn sendMessageToTarget(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const Params = struct {
fn sendMessageToTarget(cmd: anytype) !void {
const params = (try cmd.params(struct {
message: []const u8,
sessionId: []const u8,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s} ({s})", .{ input.id, "target.sendMessageToTarget", input.params.message });
})) orelse return error.InvalidParams;
// get the wrapped message.
var wmsg = IncomingMessage.init(alloc, input.params.message);
defer wmsg.deinit();
const Capture = struct {
allocator: std.mem.Allocator,
buf: std.ArrayListUnmanaged(u8),
const res = cdp.dispatch(alloc, &wmsg, ctx) catch |e| {
log.err("send message {d} ({s}): {any}", .{ input.id, input.params.message, e });
// TODO dispatch error correctly.
return e;
pub fn sendJSON(self: *@This(), message: anytype) !void {
return std.json.stringify(message, .{
.emit_null_optional_fields = false,
}, self.buf.writer(self.allocator));
}
};
// receivedMessageFromTarget event
const ReceivedMessageFromTarget = struct {
message: []const u8,
sessionId: []const u8,
var capture = Capture{
.buf = .{},
.allocator = cmd.arena,
};
try cdp.sendEvent(
alloc,
ctx,
"Target.receivedMessageFromTarget",
ReceivedMessageFromTarget,
.{
.message = res,
.sessionId = input.params.sessionId,
},
null,
);
return "";
cmd.cdp.dispatch(cmd.arena, &capture, params.message) catch |err| {
log.err("send message {d} ({s}): {any}", .{ cmd.id orelse -1, params.message, err });
return err;
};
try cmd.sendEvent("Target.receivedMessageFromTarget", .{
.message = capture.buf.items,
.sessionId = params.sessionId,
}, .{});
}
// noop
fn detachFromTarget(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "target.detachFromTarget" });
// output
return result(alloc, input.id, bool, true, input.sessionId);
fn detachFromTarget(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}

View File

@@ -261,12 +261,13 @@ pub fn main() !void {
defer loop.deinit();
// browser
var browser = Browser{};
try Browser.init(&browser, alloc, &loop, vm);
var browser = Browser.init(alloc, &loop);
defer browser.deinit();
var session = try browser.newSession({});
// page
const page = try browser.session.createPage();
const page = try session.createPage();
try page.start(null);
defer page.end();

View File

@@ -336,7 +336,6 @@ test {
std.testing.refAllDecls(queryTest);
std.testing.refAllDecls(@import("generate.zig"));
std.testing.refAllDecls(@import("cdp/msg.zig"));
// Don't use refAllDecls, as this will pull in the entire project
// and break the test build.

File diff suppressed because it is too large Load Diff

View File

@@ -334,14 +334,12 @@ test {
std.testing.refAllDecls(@import("browser/dump.zig"));
std.testing.refAllDecls(@import("browser/loader.zig"));
std.testing.refAllDecls(@import("browser/mime.zig"));
std.testing.refAllDecls(@import("cdp/msg.zig"));
std.testing.refAllDecls(@import("css/css.zig"));
std.testing.refAllDecls(@import("css/libdom_test.zig"));
std.testing.refAllDecls(@import("css/match_test.zig"));
std.testing.refAllDecls(@import("css/parser.zig"));
std.testing.refAllDecls(@import("generate.zig"));
std.testing.refAllDecls(@import("http/Client.zig"));
std.testing.refAllDecls(@import("msg.zig"));
std.testing.refAllDecls(@import("storage/storage.zig"));
std.testing.refAllDecls(@import("iterator/iterator.zig"));
std.testing.refAllDecls(@import("server.zig"));
}