From 0a04eabc5727010f4b33d7be2a01db24c717afe5 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 30 Mar 2026 13:04:14 +0800 Subject: [PATCH] Removing remaining CDP generic Follow up to https://github.com/lightpanda-io/browser/pull/1990 which makes both BrowserContext and Command non-generic. --- src/cdp/CDP.zig | 951 +++++++++++++++--------------- src/cdp/domains/accessibility.zig | 9 +- src/cdp/domains/browser.zig | 17 +- src/cdp/domains/css.zig | 3 +- src/cdp/domains/dom.zig | 45 +- src/cdp/domains/emulation.zig | 13 +- src/cdp/domains/fetch.zig | 24 +- src/cdp/domains/input.zig | 9 +- src/cdp/domains/inspector.zig | 3 +- src/cdp/domains/log.zig | 3 +- src/cdp/domains/lp.zig | 28 +- src/cdp/domains/network.zig | 37 +- src/cdp/domains/page.zig | 41 +- src/cdp/domains/performance.zig | 3 +- src/cdp/domains/runtime.zig | 8 +- src/cdp/domains/security.zig | 5 +- src/cdp/domains/storage.zig | 11 +- src/cdp/domains/target.zig | 51 +- src/cdp/testing.zig | 2 +- 19 files changed, 642 insertions(+), 621 deletions(-) diff --git a/src/cdp/CDP.zig b/src/cdp/CDP.zig index 21005de4..bfdc06dc 100644 --- a/src/cdp/CDP.zig +++ b/src/cdp/CDP.zig @@ -63,7 +63,7 @@ target_id_gen: TargetIdGen = .{}, session_id_gen: SessionIdGen = .{}, browser_context_id_gen: BrowserContextIdGen = .{}, -browser_context: ?BrowserContext(CDP), +browser_context: ?BrowserContext, // Re-used arena for processing a message. We're assuming that we're getting // 1 message at a time. @@ -120,7 +120,7 @@ pub fn handleMessage(self: *CDP, msg: []const u8) bool { pub fn processMessage(self: *CDP, msg: []const u8) !void { const arena = &self.message_arena; defer _ = arena.reset(.{ .retain_with_limit = 1024 * 16 }); - return self.dispatch(arena.allocator(), self, msg); + return self.dispatch(arena.allocator(), .{ .cdp = self }, msg); } // @newhttp @@ -136,12 +136,12 @@ pub fn pageWait(self: *CDP, ms: u32) !Session.Runner.CDPWaitResult { // Called from above, in processMessage which handles client messages // but can also be called internally. For example, Target.sendMessageToTarget // calls back into dispatch to capture the response. -pub fn dispatch(self: *CDP, arena: Allocator, sender: anytype, str: []const u8) !void { +pub fn dispatch(self: *CDP, arena: Allocator, sender: Command.Sender, str: []const u8) !void { const input = json.parseFromSliceLeaky(InputMessage, arena, str, .{ .ignore_unknown_fields = true, }) catch return error.InvalidJSON; - var command = Command(CDP, @TypeOf(sender)){ + var command = Command{ .input = .{ .json = str, .id = input.id, @@ -186,7 +186,7 @@ pub fn dispatch(self: *CDP, arena: Allocator, sender: anytype, str: []const u8) // "special" handling - the bare minimum we need to do until the driver // switches to a real BrowserContext. // (I can imagine this logic will become driver-specific) -fn dispatchStartupCommand(command: anytype, method: []const u8) !void { +fn dispatchStartupCommand(command: *Command, method: []const u8) !void { // Stagehand parses the response and error if we don't return a // correct one for Page.getFrameTree on startup call. if (std.mem.eql(u8, method, "Page.getFrameTree")) { @@ -197,7 +197,7 @@ fn dispatchStartupCommand(command: anytype, method: []const u8) !void { return command.sendResult(null, .{}); } -fn dispatchCommand(command: anytype, method: []const u8) !void { +fn dispatchCommand(command: *Command, method: []const u8) !void { const domain = blk: { const i = std.mem.indexOfScalarPos(u8, method, 0, '.') orelse { return error.InvalidMethod; @@ -273,10 +273,10 @@ pub fn createBrowserContext(self: *CDP) ![]const u8 { } const id = self.browser_context_id_gen.next(); - self.browser_context = @as(BrowserContext(CDP), undefined); + self.browser_context = @as(BrowserContext, undefined); const browser_context = &self.browser_context.?; - try BrowserContext(CDP).init(browser_context, id, self); + try BrowserContext.init(browser_context, id, self); return id; } @@ -308,7 +308,7 @@ pub fn sendJSON(self: *CDP, message: anytype) !void { }); } -pub fn BrowserContext(comptime CDP_T: type) type { +pub const BrowserContext = struct { const Node = @import("Node.zig"); const AXNode = @import("AXNode.zig"); @@ -317,450 +317,446 @@ pub fn BrowserContext(comptime CDP_T: type) type { data: std.ArrayList(u8), }; - return struct { - id: []const u8, - cdp: *CDP_T, + id: []const u8, + cdp: *CDP, - // Represents the browser session. There is no equivalent in CDP. For - // all intents and purpose, from CDP's point of view our Browser and - // our Session more or less maps to a BrowserContext. THIS HAS ZERO - // RELATION TO SESSION_ID - session: *Session, + // Represents the browser session. There is no equivalent in CDP. For + // all intents and purpose, from CDP's point of view our Browser and + // our Session more or less maps to a BrowserContext. THIS HAS ZERO + // RELATION TO SESSION_ID + session: *Session, - // Tied to the lifetime of the BrowserContext - arena: Allocator, + // Tied to the lifetime of the BrowserContext + arena: Allocator, - // Tied to the lifetime of 1 page rendered in the BrowserContext. - page_arena: Allocator, + // Tied to the lifetime of 1 page rendered in the BrowserContext. + page_arena: Allocator, - // From the parent's notification_arena.allocator(). Most of the CDP - // code paths deal with a cmd which has its own arena (from the - // message_arena). But notifications happen outside of the typical CDP - // request->response, and thus don't have a cmd and don't have an arena. - notification_arena: Allocator, + // From the parent's notification_arena.allocator(). Most of the CDP + // code paths deal with a cmd which has its own arena (from the + // message_arena). But notifications happen outside of the typical CDP + // request->response, and thus don't have a cmd and don't have an arena. + notification_arena: Allocator, - // Maps to our Page. (There are other types of targets, but we only - // deal with "pages" for now). Since we only allow 1 open page at a - // time, we only have 1 target_id. - target_id: ?[14]u8, + // Maps to our Page. (There are other types of targets, but we only + // deal with "pages" for now). Since we only allow 1 open page at a + // time, we only have 1 target_id. + target_id: ?[14]u8, - // The CDP session_id. After the target/page is created, the client - // "attaches" to it (either explicitly or automatically). We return a - // "sessionId" which identifies this link. `sessionId` is the how - // the CDP client informs us what it's trying to manipulate. Because we - // only support 1 BrowserContext at a time, and 1 page at a time, this - // is all pretty straightforward, but it still needs to be enforced, i.e. - // if we get a request with a sessionId that doesn't match the current one - // we should reject it. - session_id: ?[]const u8, + // The CDP session_id. After the target/page is created, the client + // "attaches" to it (either explicitly or automatically). We return a + // "sessionId" which identifies this link. `sessionId` is the how + // the CDP client informs us what it's trying to manipulate. Because we + // only support 1 BrowserContext at a time, and 1 page at a time, this + // is all pretty straightforward, but it still needs to be enforced, i.e. + // if we get a request with a sessionId that doesn't match the current one + // we should reject it. + session_id: ?[]const u8, - security_origin: []const u8, - page_life_cycle_events: bool, - secure_context_type: []const u8, - node_registry: Node.Registry, - node_search_list: Node.Search.List, + security_origin: []const u8, + page_life_cycle_events: bool, + secure_context_type: []const u8, + node_registry: Node.Registry, + node_search_list: Node.Search.List, - inspector_session: *js.Inspector.Session, - isolated_worlds: std.ArrayList(*IsolatedWorld), + inspector_session: *js.Inspector.Session, + isolated_worlds: std.ArrayList(*IsolatedWorld), - // Scripts registered via Page.addScriptToEvaluateOnNewDocument. - // Evaluated in each new document after navigation completes. - scripts_on_new_document: std.ArrayList(ScriptOnNewDocument) = .empty, - next_script_id: u32 = 1, + // Scripts registered via Page.addScriptToEvaluateOnNewDocument. + // Evaluated in each new document after navigation completes. + scripts_on_new_document: std.ArrayList(ScriptOnNewDocument) = .empty, + next_script_id: u32 = 1, - http_proxy_changed: bool = false, + http_proxy_changed: bool = false, - // Extra headers to add to all requests. - extra_headers: std.ArrayList([*c]const u8) = .empty, + // Extra headers to add to all requests. + extra_headers: std.ArrayList([*c]const u8) = .empty, - intercept_state: InterceptState, + intercept_state: InterceptState, - // When network is enabled, we'll capture the transfer.id -> body - // This is awfully memory intensive, but our underlying http client and - // its users (script manager and page) correctly do not hold the body - // memory longer than they have to. In fact, the main request is only - // ever streamed. So if CDP is the only thing that needs bodies in - // memory for an arbitrary amount of time, then that's where we're going - // to store the, - captured_responses: std.AutoHashMapUnmanaged(usize, CapturedResponse), + // When network is enabled, we'll capture the transfer.id -> body + // This is awfully memory intensive, but our underlying http client and + // its users (script manager and page) correctly do not hold the body + // memory longer than they have to. In fact, the main request is only + // ever streamed. So if CDP is the only thing that needs bodies in + // memory for an arbitrary amount of time, then that's where we're going + // to store the, + captured_responses: std.AutoHashMapUnmanaged(usize, CapturedResponse), - notification: *Notification, + notification: *Notification, - const Self = @This(); + fn init(self: *BrowserContext, id: []const u8, cdp: *CDP) !void { + const allocator = cdp.allocator; - fn init(self: *Self, id: []const u8, cdp: *CDP_T) !void { - const allocator = cdp.allocator; + // Create notification for this BrowserContext + const notification = try Notification.init(allocator); + errdefer notification.deinit(); - // Create notification for this BrowserContext - const notification = try Notification.init(allocator); - errdefer notification.deinit(); + const session = try cdp.browser.newSession(notification); - const session = try cdp.browser.newSession(notification); + const browser = &cdp.browser; + const inspector_session = browser.env.inspector.?.startSession(self); + errdefer browser.env.inspector.?.stopSession(); - const browser = &cdp.browser; - const inspector_session = browser.env.inspector.?.startSession(self); - errdefer browser.env.inspector.?.stopSession(); + var registry = Node.Registry.init(allocator); + errdefer registry.deinit(); - var registry = Node.Registry.init(allocator); - errdefer registry.deinit(); + self.* = .{ + .id = id, + .cdp = cdp, + .target_id = null, + .session_id = null, + .session = session, + .security_origin = URL_BASE, + .secure_context_type = "Secure", // TODO = enum + .page_life_cycle_events = false, // TODO; Target based value + .node_registry = registry, + .node_search_list = undefined, + .isolated_worlds = .empty, + .inspector_session = inspector_session, + .page_arena = cdp.page_arena.allocator(), + .arena = cdp.browser_context_arena.allocator(), + .notification_arena = cdp.notification_arena.allocator(), + .intercept_state = try InterceptState.init(allocator), + .captured_responses = .empty, + .notification = notification, + }; + self.node_search_list = Node.Search.List.init(allocator, &self.node_registry); + errdefer self.deinit(); - self.* = .{ - .id = id, - .cdp = cdp, - .target_id = null, - .session_id = null, - .session = session, - .security_origin = URL_BASE, - .secure_context_type = "Secure", // TODO = enum - .page_life_cycle_events = false, // TODO; Target based value - .node_registry = registry, - .node_search_list = undefined, - .isolated_worlds = .empty, - .inspector_session = inspector_session, - .page_arena = cdp.page_arena.allocator(), - .arena = cdp.browser_context_arena.allocator(), - .notification_arena = cdp.notification_arena.allocator(), - .intercept_state = try InterceptState.init(allocator), - .captured_responses = .empty, - .notification = notification, - }; - self.node_search_list = Node.Search.List.init(allocator, &self.node_registry); - errdefer self.deinit(); + try notification.register(.page_remove, self, onPageRemove); + try notification.register(.page_created, self, onPageCreated); + try notification.register(.page_navigate, self, onPageNavigate); + try notification.register(.page_navigated, self, onPageNavigated); + try notification.register(.page_frame_created, self, onPageFrameCreated); + } - try notification.register(.page_remove, self, onPageRemove); - try notification.register(.page_created, self, onPageCreated); - try notification.register(.page_navigate, self, onPageNavigate); - try notification.register(.page_navigated, self, onPageNavigated); - try notification.register(.page_frame_created, self, onPageFrameCreated); + pub fn deinit(self: *BrowserContext) void { + const browser = &self.cdp.browser; + const env = &browser.env; + + // resetContextGroup detach the inspector from all contexts. + // It appends async tasks, so we make sure we run the message loop + // before deinit it. + env.inspector.?.resetContextGroup(); + env.inspector.?.stopSession(); + + // abort all intercepted requests before closing the sesion/page + // since some of these might callback into the page/scriptmanager + for (self.intercept_state.pendingTransfers()) |transfer| { + transfer.abort(error.ClientDisconnect); } - pub fn deinit(self: *Self) void { - const browser = &self.cdp.browser; - const env = &browser.env; - - // resetContextGroup detach the inspector from all contexts. - // It appends async tasks, so we make sure we run the message loop - // before deinit it. - env.inspector.?.resetContextGroup(); - env.inspector.?.stopSession(); - - // abort all intercepted requests before closing the sesion/page - // since some of these might callback into the page/scriptmanager - for (self.intercept_state.pendingTransfers()) |transfer| { - transfer.abort(error.ClientDisconnect); - } - - for (self.isolated_worlds.items) |world| { - world.deinit(); - } - self.isolated_worlds.clearRetainingCapacity(); - - // do this before closeSession, since we don't want to process any - // new notification (Or maybe, instead of the deinit above, we just - // rely on those notifications to do our normal cleanup?) - - self.notification.unregisterAll(self); - - // If the session has a page, we need to clear it first. The page - // context is always nested inside of the isolated world context, - // so we need to shutdown the page one first. - browser.closeSession(); - - self.node_registry.deinit(); - self.node_search_list.deinit(); - self.notification.deinit(); - - if (self.http_proxy_changed) { - // has to be called after browser.closeSession, since it won't - // work if there are active connections. - browser.http_client.changeProxy(null) catch |err| { - log.warn(.http, "changeProxy", .{ .err = err }); - }; - } - self.intercept_state.deinit(); + for (self.isolated_worlds.items) |world| { + world.deinit(); } + self.isolated_worlds.clearRetainingCapacity(); - pub fn reset(self: *Self) void { - self.node_registry.reset(); - self.node_search_list.reset(); - } + // do this before closeSession, since we don't want to process any + // new notification (Or maybe, instead of the deinit above, we just + // rely on those notifications to do our normal cleanup?) - pub fn createIsolatedWorld(self: *Self, world_name: []const u8, grant_universal_access: bool) !*IsolatedWorld { - const browser = &self.cdp.browser; - const arena = try browser.arena_pool.acquire(.{ .debug = "IsolatedWorld" }); - errdefer browser.arena_pool.release(arena); + self.notification.unregisterAll(self); - const call_arena = try browser.arena_pool.acquire(.{ .debug = "IsolatedWorld.call_arena" }); - errdefer browser.arena_pool.release(call_arena); + // If the session has a page, we need to clear it first. The page + // context is always nested inside of the isolated world context, + // so we need to shutdown the page one first. + browser.closeSession(); - const world = try arena.create(IsolatedWorld); - world.* = .{ - .arena = arena, - .call_arena = call_arena, - .context = null, - .browser = browser, - .name = try arena.dupe(u8, world_name), - .grant_universal_access = grant_universal_access, - }; + self.node_registry.deinit(); + self.node_search_list.deinit(); + self.notification.deinit(); - try self.isolated_worlds.append(self.arena, world); - - return world; - } - - pub fn nodeWriter(self: *Self, root: *const Node, opts: Node.Writer.Opts) Node.Writer { - return .{ - .root = root, - .depth = opts.depth, - .exclude_root = opts.exclude_root, - .registry = &self.node_registry, + if (self.http_proxy_changed) { + // has to be called after browser.closeSession, since it won't + // work if there are active connections. + browser.http_client.changeProxy(null) catch |err| { + log.warn(.http, "changeProxy", .{ .err = err }); }; } + self.intercept_state.deinit(); + } - pub fn axnodeWriter(self: *Self, root: *const Node, opts: AXNode.Writer.Opts) !AXNode.Writer { - const page = self.session.currentPage() orelse return error.PageNotLoaded; - _ = opts; - return .{ - .page = page, - .root = root, - .registry = &self.node_registry, - }; + pub fn reset(self: *BrowserContext) void { + self.node_registry.reset(); + self.node_search_list.reset(); + } + + pub fn createIsolatedWorld(self: *BrowserContext, world_name: []const u8, grant_universal_access: bool) !*IsolatedWorld { + const browser = &self.cdp.browser; + const arena = try browser.arena_pool.acquire(.{ .debug = "IsolatedWorld" }); + errdefer browser.arena_pool.release(arena); + + const call_arena = try browser.arena_pool.acquire(.{ .debug = "IsolatedWorld.call_arena" }); + errdefer browser.arena_pool.release(call_arena); + + const world = try arena.create(IsolatedWorld); + world.* = .{ + .arena = arena, + .call_arena = call_arena, + .context = null, + .browser = browser, + .name = try arena.dupe(u8, world_name), + .grant_universal_access = grant_universal_access, + }; + + try self.isolated_worlds.append(self.arena, world); + + return world; + } + + pub fn nodeWriter(self: *BrowserContext, root: *const Node, opts: Node.Writer.Opts) Node.Writer { + return .{ + .root = root, + .depth = opts.depth, + .exclude_root = opts.exclude_root, + .registry = &self.node_registry, + }; + } + + pub fn axnodeWriter(self: *BrowserContext, root: *const Node, opts: AXNode.Writer.Opts) !AXNode.Writer { + const page = self.session.currentPage() orelse return error.PageNotLoaded; + _ = opts; + return .{ + .page = page, + .root = root, + .registry = &self.node_registry, + }; + } + + pub fn getURL(self: *const BrowserContext) ?[:0]const u8 { + const page = self.session.currentPage() orelse return null; + const url = page.url; + return if (url.len == 0) null else url; + } + + pub fn getTitle(self: *const BrowserContext) ?[]const u8 { + const page = self.session.currentPage() orelse return null; + return page.getTitle() catch |err| { + log.err(.cdp, "page title", .{ .err = err }); + return null; + }; + } + + pub fn networkEnable(self: *BrowserContext) !void { + try self.notification.register(.http_request_fail, self, onHttpRequestFail); + try self.notification.register(.http_request_start, self, onHttpRequestStart); + try self.notification.register(.http_request_done, self, onHttpRequestDone); + try self.notification.register(.http_response_data, self, onHttpResponseData); + try self.notification.register(.http_response_header_done, self, onHttpResponseHeadersDone); + } + + pub fn networkDisable(self: *BrowserContext) void { + self.notification.unregister(.http_request_fail, self); + self.notification.unregister(.http_request_start, self); + self.notification.unregister(.http_request_done, self); + self.notification.unregister(.http_response_data, self); + self.notification.unregister(.http_response_header_done, self); + } + + pub fn fetchEnable(self: *BrowserContext, authRequests: bool) !void { + try self.notification.register(.http_request_intercept, self, onHttpRequestIntercept); + if (authRequests) { + try self.notification.register(.http_request_auth_required, self, onHttpRequestAuthRequired); } + } - pub fn getURL(self: *const Self) ?[:0]const u8 { - const page = self.session.currentPage() orelse return null; - const url = page.url; - return if (url.len == 0) null else url; - } + pub fn fetchDisable(self: *BrowserContext) void { + self.notification.unregister(.http_request_intercept, self); + self.notification.unregister(.http_request_auth_required, self); + } - pub fn getTitle(self: *const Self) ?[]const u8 { - const page = self.session.currentPage() orelse return null; - return page.getTitle() catch |err| { - log.err(.cdp, "page title", .{ .err = err }); - return null; - }; - } + pub fn lifecycleEventsEnable(self: *BrowserContext) !void { + self.page_life_cycle_events = true; + try self.notification.register(.page_network_idle, self, onPageNetworkIdle); + try self.notification.register(.page_network_almost_idle, self, onPageNetworkAlmostIdle); + } - pub fn networkEnable(self: *Self) !void { - try self.notification.register(.http_request_fail, self, onHttpRequestFail); - try self.notification.register(.http_request_start, self, onHttpRequestStart); - try self.notification.register(.http_request_done, self, onHttpRequestDone); - try self.notification.register(.http_response_data, self, onHttpResponseData); - try self.notification.register(.http_response_header_done, self, onHttpResponseHeadersDone); - } + pub fn lifecycleEventsDisable(self: *BrowserContext) void { + self.page_life_cycle_events = false; + self.notification.unregister(.page_network_idle, self); + self.notification.unregister(.page_network_almost_idle, self); + } - pub fn networkDisable(self: *Self) void { - self.notification.unregister(.http_request_fail, self); - self.notification.unregister(.http_request_start, self); - self.notification.unregister(.http_request_done, self); - self.notification.unregister(.http_response_data, self); - self.notification.unregister(.http_response_header_done, self); - } + pub fn onPageRemove(ctx: *anyopaque, _: Notification.PageRemove) !void { + const self: *BrowserContext = @ptrCast(@alignCast(ctx)); + try @import("domains/page.zig").pageRemove(self); + } - pub fn fetchEnable(self: *Self, authRequests: bool) !void { - try self.notification.register(.http_request_intercept, self, onHttpRequestIntercept); - if (authRequests) { - try self.notification.register(.http_request_auth_required, self, onHttpRequestAuthRequired); - } - } + pub fn onPageCreated(ctx: *anyopaque, page: *Page) !void { + const self: *BrowserContext = @ptrCast(@alignCast(ctx)); + return @import("domains/page.zig").pageCreated(self, page); + } - pub fn fetchDisable(self: *Self) void { - self.notification.unregister(.http_request_intercept, self); - self.notification.unregister(.http_request_auth_required, self); - } + pub fn onPageNavigate(ctx: *anyopaque, msg: *const Notification.PageNavigate) !void { + const self: *BrowserContext = @ptrCast(@alignCast(ctx)); + return @import("domains/page.zig").pageNavigate(self, msg); + } - pub fn lifecycleEventsEnable(self: *Self) !void { - self.page_life_cycle_events = true; - try self.notification.register(.page_network_idle, self, onPageNetworkIdle); - try self.notification.register(.page_network_almost_idle, self, onPageNetworkAlmostIdle); - } + pub fn onPageNavigated(ctx: *anyopaque, msg: *const Notification.PageNavigated) !void { + const self: *BrowserContext = @ptrCast(@alignCast(ctx)); + defer self.resetNotificationArena(); + return @import("domains/page.zig").pageNavigated(self.notification_arena, self, msg); + } - pub fn lifecycleEventsDisable(self: *Self) void { - self.page_life_cycle_events = false; - self.notification.unregister(.page_network_idle, self); - self.notification.unregister(.page_network_almost_idle, self); - } + pub fn onPageFrameCreated(ctx: *anyopaque, msg: *const Notification.PageFrameCreated) !void { + const self: *BrowserContext = @ptrCast(@alignCast(ctx)); + return @import("domains/page.zig").pageFrameCreated(self, msg); + } - pub fn onPageRemove(ctx: *anyopaque, _: Notification.PageRemove) !void { - const self: *Self = @ptrCast(@alignCast(ctx)); - try @import("domains/page.zig").pageRemove(self); - } + pub fn onPageNetworkIdle(ctx: *anyopaque, msg: *const Notification.PageNetworkIdle) !void { + const self: *BrowserContext = @ptrCast(@alignCast(ctx)); + return @import("domains/page.zig").pageNetworkIdle(self, msg); + } - pub fn onPageCreated(ctx: *anyopaque, page: *Page) !void { - const self: *Self = @ptrCast(@alignCast(ctx)); - return @import("domains/page.zig").pageCreated(self, page); - } + pub fn onPageNetworkAlmostIdle(ctx: *anyopaque, msg: *const Notification.PageNetworkAlmostIdle) !void { + const self: *BrowserContext = @ptrCast(@alignCast(ctx)); + return @import("domains/page.zig").pageNetworkAlmostIdle(self, msg); + } - pub fn onPageNavigate(ctx: *anyopaque, msg: *const Notification.PageNavigate) !void { - const self: *Self = @ptrCast(@alignCast(ctx)); - return @import("domains/page.zig").pageNavigate(self, msg); - } + pub fn onHttpRequestStart(ctx: *anyopaque, msg: *const Notification.RequestStart) !void { + const self: *BrowserContext = @ptrCast(@alignCast(ctx)); + try @import("domains/network.zig").httpRequestStart(self, msg); + } - pub fn onPageNavigated(ctx: *anyopaque, msg: *const Notification.PageNavigated) !void { - const self: *Self = @ptrCast(@alignCast(ctx)); - defer self.resetNotificationArena(); - return @import("domains/page.zig").pageNavigated(self.notification_arena, self, msg); - } + pub fn onHttpRequestIntercept(ctx: *anyopaque, msg: *const Notification.RequestIntercept) !void { + const self: *BrowserContext = @ptrCast(@alignCast(ctx)); + try @import("domains/fetch.zig").requestIntercept(self, msg); + } - pub fn onPageFrameCreated(ctx: *anyopaque, msg: *const Notification.PageFrameCreated) !void { - const self: *Self = @ptrCast(@alignCast(ctx)); - return @import("domains/page.zig").pageFrameCreated(self, msg); - } + pub fn onHttpRequestFail(ctx: *anyopaque, msg: *const Notification.RequestFail) !void { + const self: *BrowserContext = @ptrCast(@alignCast(ctx)); + return @import("domains/network.zig").httpRequestFail(self, msg); + } - pub fn onPageNetworkIdle(ctx: *anyopaque, msg: *const Notification.PageNetworkIdle) !void { - const self: *Self = @ptrCast(@alignCast(ctx)); - return @import("domains/page.zig").pageNetworkIdle(self, msg); - } + pub fn onHttpResponseHeadersDone(ctx: *anyopaque, msg: *const Notification.ResponseHeaderDone) !void { + const self: *BrowserContext = @ptrCast(@alignCast(ctx)); + defer self.resetNotificationArena(); - pub fn onPageNetworkAlmostIdle(ctx: *anyopaque, msg: *const Notification.PageNetworkAlmostIdle) !void { - const self: *Self = @ptrCast(@alignCast(ctx)); - return @import("domains/page.zig").pageNetworkAlmostIdle(self, msg); - } + const arena = self.page_arena; - pub fn onHttpRequestStart(ctx: *anyopaque, msg: *const Notification.RequestStart) !void { - const self: *Self = @ptrCast(@alignCast(ctx)); - try @import("domains/network.zig").httpRequestStart(self, msg); - } + // Prepare the captured response value. + const id = msg.transfer.id; + const gop = try self.captured_responses.getOrPut(arena, id); + if (!gop.found_existing) { + gop.value_ptr.* = .{ + .data = .empty, + // Encode the data in base64 by default, but don't encode + // for well known content-type. + .must_encode = blk: { + const transfer = msg.transfer; + if (transfer.response_header.?.contentType()) |ct| { + const mime = try Mime.parse(ct); - pub fn onHttpRequestIntercept(ctx: *anyopaque, msg: *const Notification.RequestIntercept) !void { - const self: *Self = @ptrCast(@alignCast(ctx)); - try @import("domains/fetch.zig").requestIntercept(self, msg); - } - - pub fn onHttpRequestFail(ctx: *anyopaque, msg: *const Notification.RequestFail) !void { - const self: *Self = @ptrCast(@alignCast(ctx)); - return @import("domains/network.zig").httpRequestFail(self, msg); - } - - pub fn onHttpResponseHeadersDone(ctx: *anyopaque, msg: *const Notification.ResponseHeaderDone) !void { - const self: *Self = @ptrCast(@alignCast(ctx)); - defer self.resetNotificationArena(); - - const arena = self.page_arena; - - // Prepare the captured response value. - const id = msg.transfer.id; - const gop = try self.captured_responses.getOrPut(arena, id); - if (!gop.found_existing) { - gop.value_ptr.* = .{ - .data = .empty, - // Encode the data in base64 by default, but don't encode - // for well known content-type. - .must_encode = blk: { - const transfer = msg.transfer; - if (transfer.response_header.?.contentType()) |ct| { - const mime = try Mime.parse(ct); - - if (!mime.isText()) { - break :blk true; - } - - if (std.mem.eql(u8, "UTF-8", mime.charsetString())) { - break :blk false; - } + if (!mime.isText()) { + break :blk true; } - break :blk true; - }, - }; - } - return @import("domains/network.zig").httpResponseHeaderDone(self.notification_arena, self, msg); - } - - pub fn onHttpRequestDone(ctx: *anyopaque, msg: *const Notification.RequestDone) !void { - const self: *Self = @ptrCast(@alignCast(ctx)); - return @import("domains/network.zig").httpRequestDone(self, msg); - } - - pub fn onHttpResponseData(ctx: *anyopaque, msg: *const Notification.ResponseData) !void { - const self: *Self = @ptrCast(@alignCast(ctx)); - const arena = self.page_arena; - - const id = msg.transfer.id; - const resp = self.captured_responses.getPtr(id) orelse lp.assert(false, "onHttpResponseData missinf captured response", .{}); - - return resp.data.appendSlice(arena, msg.data); - } - - pub fn onHttpRequestAuthRequired(ctx: *anyopaque, data: *const Notification.RequestAuthRequired) !void { - const self: *Self = @ptrCast(@alignCast(ctx)); - defer self.resetNotificationArena(); - try @import("domains/fetch.zig").requestAuthRequired(self, data); - } - - fn resetNotificationArena(self: *Self) void { - defer _ = self.cdp.notification_arena.reset(.{ .retain_with_limit = 1024 * 64 }); - } - - pub fn callInspector(self: *const Self, msg: []const u8) void { - self.inspector_session.send(msg); - self.session.browser.env.runMicrotasks(); - } - - pub fn onInspectorResponse(ctx: *anyopaque, _: u32, msg: []const u8) void { - sendInspectorMessage(@ptrCast(@alignCast(ctx)), msg) catch |err| { - log.err(.cdp, "send inspector response", .{ .err = err }); + if (std.mem.eql(u8, "UTF-8", mime.charsetString())) { + break :blk false; + } + } + break :blk true; + }, }; } - pub fn onInspectorEvent(ctx: *anyopaque, msg: []const u8) void { - if (log.enabled(.cdp, .debug)) { - // msg should be {"method":,... - lp.assert(std.mem.startsWith(u8, msg, "{\"method\":"), "onInspectorEvent prefix", .{}); - const method_end = std.mem.indexOfScalar(u8, msg, ',') orelse { - log.err(.cdp, "invalid inspector event", .{ .msg = msg }); - return; - }; - const method = msg[10..method_end]; - log.debug(.cdp, "inspector event", .{ .method = method }); - } + return @import("domains/network.zig").httpResponseHeaderDone(self.notification_arena, self, msg); + } - sendInspectorMessage(@ptrCast(@alignCast(ctx)), msg) catch |err| { - log.err(.cdp, "send inspector event", .{ .err = err }); - }; - } + pub fn onHttpRequestDone(ctx: *anyopaque, msg: *const Notification.RequestDone) !void { + const self: *BrowserContext = @ptrCast(@alignCast(ctx)); + return @import("domains/network.zig").httpRequestDone(self, msg); + } - // This is hacky x 2. First, we create the JSON payload by gluing our - // session_id onto it. Second, we're much more client/websocket aware than - // we should be. - fn sendInspectorMessage(self: *Self, msg: []const u8) !void { - const session_id = self.session_id orelse { - // We no longer have an active session. What should we do - // in this case? + pub fn onHttpResponseData(ctx: *anyopaque, msg: *const Notification.ResponseData) !void { + const self: *BrowserContext = @ptrCast(@alignCast(ctx)); + const arena = self.page_arena; + + const id = msg.transfer.id; + const resp = self.captured_responses.getPtr(id) orelse lp.assert(false, "onHttpResponseData missinf captured response", .{}); + + return resp.data.appendSlice(arena, msg.data); + } + + pub fn onHttpRequestAuthRequired(ctx: *anyopaque, data: *const Notification.RequestAuthRequired) !void { + const self: *BrowserContext = @ptrCast(@alignCast(ctx)); + defer self.resetNotificationArena(); + try @import("domains/fetch.zig").requestAuthRequired(self, data); + } + + fn resetNotificationArena(self: *BrowserContext) void { + defer _ = self.cdp.notification_arena.reset(.{ .retain_with_limit = 1024 * 64 }); + } + + pub fn callInspector(self: *const BrowserContext, msg: []const u8) void { + self.inspector_session.send(msg); + self.session.browser.env.runMicrotasks(); + } + + pub fn onInspectorResponse(ctx: *anyopaque, _: u32, msg: []const u8) void { + sendInspectorMessage(@ptrCast(@alignCast(ctx)), msg) catch |err| { + log.err(.cdp, "send inspector response", .{ .err = err }); + }; + } + + pub fn onInspectorEvent(ctx: *anyopaque, msg: []const u8) void { + if (log.enabled(.cdp, .debug)) { + // msg should be {"method":,... + lp.assert(std.mem.startsWith(u8, msg, "{\"method\":"), "onInspectorEvent prefix", .{}); + const method_end = std.mem.indexOfScalar(u8, msg, ',') orelse { + log.err(.cdp, "invalid inspector event", .{ .msg = msg }); return; }; - - const cdp = self.cdp; - const allocator = cdp.client.sendAllocator(); - - const field = ",\"sessionId\":\""; - - // + 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.ArrayList(u8) = .{}; - buf.ensureTotalCapacity(allocator, message_len) catch |err| { - log.err(.cdp, "inspector buffer", .{ .err = 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("\"}"); - if (comptime IS_DEBUG) { - std.debug.assert(buf.items.len == message_len); - } - - try cdp.client.sendJSONRaw(buf); + const method = msg[10..method_end]; + log.debug(.cdp, "inspector event", .{ .method = method }); } - }; -} + + sendInspectorMessage(@ptrCast(@alignCast(ctx)), msg) catch |err| { + log.err(.cdp, "send inspector event", .{ .err = err }); + }; + } + + // This is hacky x 2. First, we create the JSON payload by gluing our + // session_id onto it. Second, we're much more client/websocket aware than + // we should be. + fn sendInspectorMessage(self: *BrowserContext, msg: []const u8) !void { + const session_id = self.session_id orelse { + // We no longer have an active session. What should we do + // in this case? + return; + }; + + const cdp = self.cdp; + const allocator = cdp.client.sendAllocator(); + + const field = ",\"sessionId\":\""; + + // + 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.ArrayList(u8) = .{}; + buf.ensureTotalCapacity(allocator, message_len) catch |err| { + log.err(.cdp, "inspector buffer", .{ .err = 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("\"}"); + if (comptime IS_DEBUG) { + std.debug.assert(buf.items.len == message_len); + } + + try cdp.client.sendJSONRaw(buf); + } +}; /// see: https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/bindings/core/v8/V8BindingDesign.md#world /// The current understanding. An isolated world lives in the same isolate, but a separated context. @@ -832,96 +828,105 @@ const IsolatedWorld = struct { // 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 CDP_T: type, comptime Sender: type) type { - return struct { - // A misc arena that can be used for any allocation for processing - // the message - arena: Allocator, +pub const Command = struct { + // A misc arena that can be used for any allocation for processing + // the message + arena: Allocator, - // reference to our CDP instance - cdp: *CDP_T, + // reference to our CDP instance + cdp: *CDP, - // The browser context this command targets - browser_context: ?*BrowserContext(CDP_T), + // The browser context this command targets + browser_context: ?*BrowserContext, - // The command input (the id, optional session_id, params, ...) - input: Input, + // The command input (the id, optional session_id, params, ...) + input: Input, - // In most cases, Sender is going to be cdp itself. We'll call - // sender.sendJSON() and CDP will send it to the client. But some - // comamnds are dispatched internally, in which cases the Sender will - // be code to capture the data that we were "sending". - sender: Sender, + // In most cases, Sender is going to be cdp itself. We'll call + // sender.sendJSON() and CDP will send it to the client. But some + // comamnds are dispatched internally, in which cases the Sender will + // be code to capture the data that we were "sending". + sender: Sender, - const Self = @This(); + const Sender = union(enum) { + cdp: *CDP, + capture: *std.Io.Writer, - pub fn params(self: *const Self, comptime T: type) !?T { - if (self.input.params) |p| { - return try json.parseFromSliceLeaky( - T, - self.arena, - p.raw, - .{ .ignore_unknown_fields = true }, - ); + pub fn sendJSON(self: Sender, message: anytype) !void { + switch (self) { + .cdp => |cdp| return cdp.sendJSON(message), + .capture => |writer| { + return std.json.Stringify.value(message, .{ + .emit_null_optional_fields = false, + }, writer); + }, } - return null; } - - pub fn createBrowserContext(self: *Self) !*BrowserContext(CDP_T) { - _ = try self.cdp.createBrowserContext(); - self.browser_context = &(self.cdp.browser_context.?); - return self.browser_context.?; - } - - const SendResultOpts = struct { - include_session_id: bool = true, - }; - pub fn sendResult(self: *Self, result: anytype, opts: SendResultOpts) !void { - return self.sender.sendJSON(.{ - .id = self.input.id, - .result = if (comptime @typeInfo(@TypeOf(result)) == .null) struct {}{} else result, - .sessionId = if (opts.include_session_id) self.input.session_id else null, - }); - } - - const SendEventOpts = struct { - session_id: ?[]const u8 = null, - }; - pub fn sendEvent(self: *Self, method: []const u8, p: anytype, opts: CDP_T.SendEventOpts) !void { - // Events ALWAYS go to the client. self.sender should not be used - return self.cdp.sendEvent(method, p, opts); - } - - const SendErrorOpts = struct { - include_session_id: bool = true, - }; - pub fn sendError(self: *Self, code: i32, message: []const u8, opts: SendErrorOpts) !void { - return self.sender.sendJSON(.{ - .id = self.input.id, - .@"error" = .{ .code = code, .message = message }, - .sessionId = if (opts.include_session_id) self.input.session_id else null, - }); - } - - const Input = struct { - // When we reply to a message, we echo back the message id - id: ?i64, - - // The "action" of the message.Given a method of "LOG.enable", the - // action is "enable" - action: []const u8, - - // See notes in BrowserContext about session_id - session_id: ?[]const u8, - - // Unparsed / untyped input.params. - params: ?InputParams, - - // The full raw json input - json: []const u8, - }; }; -} + + pub fn params(self: *const Command, comptime T: type) !?T { + if (self.input.params) |p| { + return try json.parseFromSliceLeaky( + T, + self.arena, + p.raw, + .{ .ignore_unknown_fields = true }, + ); + } + return null; + } + + pub fn createBrowserContext(self: *Command) !*BrowserContext { + _ = try self.cdp.createBrowserContext(); + self.browser_context = &(self.cdp.browser_context.?); + return self.browser_context.?; + } + + const SendResultOpts = struct { + include_session_id: bool = true, + }; + pub fn sendResult(self: *Command, result: anytype, opts: SendResultOpts) !void { + return self.sender.sendJSON(.{ + .id = self.input.id, + .result = if (comptime @typeInfo(@TypeOf(result)) == .null) struct {}{} else result, + .sessionId = if (opts.include_session_id) self.input.session_id else null, + }); + } + + pub fn sendEvent(self: *Command, method: []const u8, p: anytype, opts: SendEventOpts) !void { + // Events ALWAYS go to the client. self.sender should not be used + return self.cdp.sendEvent(method, p, opts); + } + + const SendErrorOpts = struct { + include_session_id: bool = true, + }; + pub fn sendError(self: *Command, code: i32, message: []const u8, opts: SendErrorOpts) !void { + return self.sender.sendJSON(.{ + .id = self.input.id, + .@"error" = .{ .code = code, .message = message }, + .sessionId = if (opts.include_session_id) self.input.session_id else null, + }); + } + + const Input = struct { + // When we reply to a message, we echo back the message id + id: ?i64, + + // The "action" of the message.Given a method of "LOG.enable", the + // action is "enable" + action: []const u8, + + // See notes in BrowserContext about session_id + session_id: ?[]const u8, + + // Unparsed / untyped input.params. + params: ?InputParams, + + // The full raw json input + json: []const u8, + }; +}; // When we parse a JSON message from the client, this is the structure // we always expect diff --git a/src/cdp/domains/accessibility.zig b/src/cdp/domains/accessibility.zig index 864c52b5..e193d93e 100644 --- a/src/cdp/domains/accessibility.zig +++ b/src/cdp/domains/accessibility.zig @@ -18,8 +18,9 @@ const std = @import("std"); const id = @import("../id.zig"); +const CDP = @import("../CDP.zig"); -pub fn processMessage(cmd: anytype) !void { +pub fn processMessage(cmd: *CDP.Command) !void { const action = std.meta.stringToEnum(enum { enable, disable, @@ -32,15 +33,15 @@ pub fn processMessage(cmd: anytype) !void { .getFullAXTree => return getFullAXTree(cmd), } } -fn enable(cmd: anytype) !void { +fn enable(cmd: *CDP.Command) !void { return cmd.sendResult(null, .{}); } -fn disable(cmd: anytype) !void { +fn disable(cmd: *CDP.Command) !void { return cmd.sendResult(null, .{}); } -fn getFullAXTree(cmd: anytype) !void { +fn getFullAXTree(cmd: *CDP.Command) !void { const params = (try cmd.params(struct { depth: ?i32 = null, frameId: ?[]const u8 = null, diff --git a/src/cdp/domains/browser.zig b/src/cdp/domains/browser.zig index 63c087a5..a63be3fa 100644 --- a/src/cdp/domains/browser.zig +++ b/src/cdp/domains/browser.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const std = @import("std"); +const CDP = @import("../CDP.zig"); // TODO: hard coded data const PROTOCOL_VERSION = "1.3"; @@ -35,7 +36,7 @@ const PRODUCT = "Chrome/124.0.6367.29"; const JS_VERSION = "12.4.254.8"; const DEV_TOOLS_WINDOW_ID = 1923710101; -pub fn processMessage(cmd: anytype) !void { +pub fn processMessage(cmd: *CDP.Command) !void { const action = std.meta.stringToEnum(enum { getVersion, setPermission, @@ -57,7 +58,7 @@ pub fn processMessage(cmd: anytype) !void { } } -fn getVersion(cmd: anytype) !void { +fn getVersion(cmd: *CDP.Command) !void { // TODO: pre-serialize? return cmd.sendResult(.{ .protocolVersion = PROTOCOL_VERSION, @@ -69,7 +70,7 @@ fn getVersion(cmd: anytype) !void { } // TODO: noop method -fn setDownloadBehavior(cmd: anytype) !void { +fn setDownloadBehavior(cmd: *CDP.Command) !void { // const params = (try cmd.params(struct { // behavior: []const u8, // browserContextId: ?[]const u8 = null, @@ -80,7 +81,7 @@ fn setDownloadBehavior(cmd: anytype) !void { return cmd.sendResult(null, .{ .include_session_id = false }); } -fn getWindowForTarget(cmd: anytype) !void { +fn getWindowForTarget(cmd: *CDP.Command) !void { // const params = (try cmd.params(struct { // targetId: ?[]const u8 = null, // })) orelse return error.InvalidParams; @@ -91,22 +92,22 @@ fn getWindowForTarget(cmd: anytype) !void { } // TODO: noop method -fn setWindowBounds(cmd: anytype) !void { +fn setWindowBounds(cmd: *CDP.Command) !void { return cmd.sendResult(null, .{}); } // TODO: noop method -fn grantPermissions(cmd: anytype) !void { +fn grantPermissions(cmd: *CDP.Command) !void { return cmd.sendResult(null, .{}); } // TODO: noop method -fn setPermission(cmd: anytype) !void { +fn setPermission(cmd: *CDP.Command) !void { return cmd.sendResult(null, .{}); } // TODO: noop method -fn resetPermissions(cmd: anytype) !void { +fn resetPermissions(cmd: *CDP.Command) !void { return cmd.sendResult(null, .{}); } diff --git a/src/cdp/domains/css.zig b/src/cdp/domains/css.zig index dad0cebd..ea1a6801 100644 --- a/src/cdp/domains/css.zig +++ b/src/cdp/domains/css.zig @@ -17,8 +17,9 @@ // along with this program. If not, see . const std = @import("std"); +const CDP = @import("../CDP.zig"); -pub fn processMessage(cmd: anytype) !void { +pub fn processMessage(cmd: *CDP.Command) !void { const action = std.meta.stringToEnum(enum { enable, }, cmd.input.action) orelse return error.UnknownMethod; diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index 0872a94e..e209a295 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -18,17 +18,18 @@ const std = @import("std"); const id = @import("../id.zig"); -const log = @import("../../log.zig"); +const CDP = @import("../CDP.zig"); const Node = @import("../Node.zig"); + +const log = @import("../../log.zig"); +const dump = @import("../../browser/dump.zig"); +const js = @import("../../browser/js/js.zig"); const DOMNode = @import("../../browser/webapi/Node.zig"); const Selector = @import("../../browser/webapi/selector/Selector.zig"); -const dump = @import("../../browser/dump.zig"); -const js = @import("../../browser/js/js.zig"); - const Allocator = std.mem.Allocator; -pub fn processMessage(cmd: anytype) !void { +pub fn processMessage(cmd: *CDP.Command) !void { const action = std.meta.stringToEnum(enum { enable, getDocument, @@ -69,7 +70,7 @@ pub fn processMessage(cmd: anytype) !void { } // https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getDocument -fn getDocument(cmd: anytype) !void { +fn getDocument(cmd: *CDP.Command) !void { const Params = struct { // CDP documentation implies that 0 isn't valid, but it _does_ work in Chrome depth: i32 = 3, @@ -89,7 +90,7 @@ fn getDocument(cmd: anytype) !void { } // https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-performSearch -fn performSearch(cmd: anytype) !void { +fn performSearch(cmd: *CDP.Command) !void { const params = (try cmd.params(struct { query: []const u8, includeUserAgentShadowDOM: ?bool = null, @@ -116,7 +117,7 @@ fn performSearch(cmd: anytype) !void { // hierarchy of each nodes. // We dispatch event in the reverse order: from the top level to the direct parents. // We should dispatch a node only if it has never been sent. -fn dispatchSetChildNodes(cmd: anytype, dom_nodes: []const *DOMNode) !void { +fn dispatchSetChildNodes(cmd: *CDP.Command, dom_nodes: []const *DOMNode) !void { const arena = cmd.arena; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const session_id = bc.session_id orelse return error.SessionIdNotLoaded; @@ -172,7 +173,7 @@ fn dispatchSetChildNodes(cmd: anytype, dom_nodes: []const *DOMNode) !void { } // https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-discardSearchResults -fn discardSearchResults(cmd: anytype) !void { +fn discardSearchResults(cmd: *CDP.Command) !void { const params = (try cmd.params(struct { searchId: []const u8, })) orelse return error.InvalidParams; @@ -184,7 +185,7 @@ fn discardSearchResults(cmd: anytype) !void { } // https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getSearchResults -fn getSearchResults(cmd: anytype) !void { +fn getSearchResults(cmd: *CDP.Command) !void { const params = (try cmd.params(struct { searchId: []const u8, fromIndex: u32, @@ -209,7 +210,7 @@ fn getSearchResults(cmd: anytype) !void { return cmd.sendResult(.{ .nodeIds = node_ids[params.fromIndex..params.toIndex] }, .{}); } -fn querySelector(cmd: anytype) !void { +fn querySelector(cmd: *CDP.Command) !void { const params = (try cmd.params(struct { nodeId: Node.Id, selector: []const u8, @@ -235,7 +236,7 @@ fn querySelector(cmd: anytype) !void { }, .{}); } -fn querySelectorAll(cmd: anytype) !void { +fn querySelectorAll(cmd: *CDP.Command) !void { const params = (try cmd.params(struct { nodeId: Node.Id, selector: []const u8, @@ -266,7 +267,7 @@ fn querySelectorAll(cmd: anytype) !void { }, .{}); } -fn resolveNode(cmd: anytype) !void { +fn resolveNode(cmd: *CDP.Command) !void { const params = (try cmd.params(struct { nodeId: ?Node.Id = null, backendNodeId: ?u32 = null, @@ -327,7 +328,7 @@ fn resolveNode(cmd: anytype) !void { } }, .{}); } -fn describeNode(cmd: anytype) !void { +fn describeNode(cmd: *CDP.Command) !void { const params = (try cmd.params(struct { nodeId: ?Node.Id = null, backendNodeId: ?Node.Id = null, @@ -374,7 +375,7 @@ fn rectToQuad(rect: DOMNode.Element.DOMRect) Quad { }; } -fn scrollIntoViewIfNeeded(cmd: anytype) !void { +fn scrollIntoViewIfNeeded(cmd: *CDP.Command) !void { const params = (try cmd.params(struct { nodeId: ?Node.Id = null, backendNodeId: ?u32 = null, @@ -397,7 +398,7 @@ fn scrollIntoViewIfNeeded(cmd: anytype) !void { return cmd.sendResult(null, .{}); } -fn getNode(arena: Allocator, bc: anytype, node_id: ?Node.Id, backend_node_id: ?Node.Id, object_id: ?[]const u8) !*Node { +fn getNode(arena: Allocator, bc: *CDP.BrowserContext, node_id: ?Node.Id, backend_node_id: ?Node.Id, object_id: ?[]const u8) !*Node { const input_node_id = node_id orelse backend_node_id; if (input_node_id) |input_node_id_| { return bc.node_registry.lookup_by_id.get(input_node_id_) orelse return error.NodeNotFound; @@ -417,7 +418,7 @@ fn getNode(arena: Allocator, bc: anytype, node_id: ?Node.Id, backend_node_id: ?N // https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getContentQuads // Related to: https://drafts.csswg.org/cssom-view/#the-geometryutils-interface -fn getContentQuads(cmd: anytype) !void { +fn getContentQuads(cmd: *CDP.Command) !void { const params = (try cmd.params(struct { nodeId: ?Node.Id = null, backendNodeId: ?Node.Id = null, @@ -443,7 +444,7 @@ fn getContentQuads(cmd: anytype) !void { return cmd.sendResult(.{ .quads = &.{quad} }, .{}); } -fn getBoxModel(cmd: anytype) !void { +fn getBoxModel(cmd: *CDP.Command) !void { const params = (try cmd.params(struct { nodeId: ?Node.Id = null, backendNodeId: ?u32 = null, @@ -472,7 +473,7 @@ fn getBoxModel(cmd: anytype) !void { } }, .{}); } -fn requestChildNodes(cmd: anytype) !void { +fn requestChildNodes(cmd: *CDP.Command) !void { const params = (try cmd.params(struct { nodeId: Node.Id, depth: i32 = 1, @@ -496,7 +497,7 @@ fn requestChildNodes(cmd: anytype) !void { return cmd.sendResult(null, .{}); } -fn getFrameOwner(cmd: anytype) !void { +fn getFrameOwner(cmd: *CDP.Command) !void { const params = (try cmd.params(struct { frameId: []const u8, })) orelse return error.InvalidParams; @@ -512,7 +513,7 @@ fn getFrameOwner(cmd: anytype) !void { return cmd.sendResult(.{ .nodeId = node.id, .backendNodeId = node.id }, .{}); } -fn getOuterHTML(cmd: anytype) !void { +fn getOuterHTML(cmd: *CDP.Command) !void { const params = (try cmd.params(struct { nodeId: ?Node.Id = null, backendNodeId: ?Node.Id = null, @@ -534,7 +535,7 @@ fn getOuterHTML(cmd: anytype) !void { return cmd.sendResult(.{ .outerHTML = aw.written() }, .{}); } -fn requestNode(cmd: anytype) !void { +fn requestNode(cmd: *CDP.Command) !void { const params = (try cmd.params(struct { objectId: []const u8, })) orelse return error.InvalidParams; diff --git a/src/cdp/domains/emulation.zig b/src/cdp/domains/emulation.zig index 2265e2e9..4cfcd7be 100644 --- a/src/cdp/domains/emulation.zig +++ b/src/cdp/domains/emulation.zig @@ -17,9 +17,10 @@ // along with this program. If not, see . const std = @import("std"); +const CDP = @import("../CDP.zig"); const log = @import("../../log.zig"); -pub fn processMessage(cmd: anytype) !void { +pub fn processMessage(cmd: *CDP.Command) !void { const action = std.meta.stringToEnum(enum { setEmulatedMedia, setFocusEmulationEnabled, @@ -38,7 +39,7 @@ pub fn processMessage(cmd: anytype) !void { } // TODO: noop method -fn setEmulatedMedia(cmd: anytype) !void { +fn setEmulatedMedia(cmd: *CDP.Command) !void { // const input = (try const incoming.params(struct { // media: ?[]const u8 = null, // features: ?[]struct{ @@ -51,7 +52,7 @@ fn setEmulatedMedia(cmd: anytype) !void { } // TODO: noop method -fn setFocusEmulationEnabled(cmd: anytype) !void { +fn setFocusEmulationEnabled(cmd: *CDP.Command) !void { // const input = (try const incoming.params(struct { // enabled: bool, // })) orelse return error.InvalidParams; @@ -59,16 +60,16 @@ fn setFocusEmulationEnabled(cmd: anytype) !void { } // TODO: noop method -fn setDeviceMetricsOverride(cmd: anytype) !void { +fn setDeviceMetricsOverride(cmd: *CDP.Command) !void { return cmd.sendResult(null, .{}); } // TODO: noop method -fn setTouchEmulationEnabled(cmd: anytype) !void { +fn setTouchEmulationEnabled(cmd: *CDP.Command) !void { return cmd.sendResult(null, .{}); } -fn setUserAgentOverride(cmd: anytype) !void { +fn setUserAgentOverride(cmd: *CDP.Command) !void { log.info(.app, "setUserAgentOverride ignored", .{}); return cmd.sendResult(null, .{}); } diff --git a/src/cdp/domains/fetch.zig b/src/cdp/domains/fetch.zig index 310479b2..4fe45600 100644 --- a/src/cdp/domains/fetch.zig +++ b/src/cdp/domains/fetch.zig @@ -17,17 +17,19 @@ // along with this program. If not, see . const std = @import("std"); -const Allocator = std.mem.Allocator; const id = @import("../id.zig"); +const CDP = @import("../CDP.zig"); const log = @import("../../log.zig"); -const network = @import("network.zig"); const HttpClient = @import("../../browser/HttpClient.zig"); const net_http = @import("../../network/http.zig"); const Notification = @import("../../Notification.zig"); -pub fn processMessage(cmd: anytype) !void { +const network = @import("network.zig"); +const Allocator = std.mem.Allocator; + +pub fn processMessage(cmd: *CDP.Command) !void { const action = std.meta.stringToEnum(enum { disable, enable, @@ -135,13 +137,13 @@ const ErrorReason = enum { BlockedByResponse, }; -fn disable(cmd: anytype) !void { +fn disable(cmd: *CDP.Command) !void { const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; bc.fetchDisable(); return cmd.sendResult(null, .{}); } -fn enable(cmd: anytype) !void { +fn enable(cmd: *CDP.Command) !void { const params = (try cmd.params(EnableParam)) orelse EnableParam{}; if (!arePatternsSupported(params.patterns)) { log.warn(.not_implemented, "Fetch.enable", .{ .params = "pattern" }); @@ -180,7 +182,7 @@ fn arePatternsSupported(patterns: []RequestPattern) bool { return true; } -pub fn requestIntercept(bc: anytype, intercept: *const Notification.RequestIntercept) !void { +pub fn requestIntercept(bc: *CDP.BrowserContext, intercept: *const Notification.RequestIntercept) !void { // detachTarget could be called, in which case, we still have a page doing // things, but no session. const session_id = bc.session_id orelse return; @@ -215,7 +217,7 @@ pub fn requestIntercept(bc: anytype, intercept: *const Notification.RequestInter intercept.wait_for_interception.* = true; } -fn continueRequest(cmd: anytype) !void { +fn continueRequest(cmd: *CDP.Command) !void { const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const params = (try cmd.params(struct { requestId: []const u8, // INT-{d}" @@ -275,7 +277,7 @@ const AuthChallengeResponse = enum { ProvideCredentials, }; -fn continueWithAuth(cmd: anytype) !void { +fn continueWithAuth(cmd: *CDP.Command) !void { const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const params = (try cmd.params(struct { requestId: []const u8, // "INT-{d}" @@ -318,7 +320,7 @@ fn continueWithAuth(cmd: anytype) !void { return cmd.sendResult(null, .{}); } -fn fulfillRequest(cmd: anytype) !void { +fn fulfillRequest(cmd: *CDP.Command) !void { const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const params = (try cmd.params(struct { @@ -360,7 +362,7 @@ fn fulfillRequest(cmd: anytype) !void { return cmd.sendResult(null, .{}); } -fn failRequest(cmd: anytype) !void { +fn failRequest(cmd: *CDP.Command) !void { const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const params = (try cmd.params(struct { requestId: []const u8, // "INT-{d}" @@ -382,7 +384,7 @@ fn failRequest(cmd: anytype) !void { return cmd.sendResult(null, .{}); } -pub fn requestAuthRequired(bc: anytype, intercept: *const Notification.RequestAuthRequired) !void { +pub fn requestAuthRequired(bc: *CDP.BrowserContext, intercept: *const Notification.RequestAuthRequired) !void { // detachTarget could be called, in which case, we still have a page doing // things, but no session. const session_id = bc.session_id orelse return; diff --git a/src/cdp/domains/input.zig b/src/cdp/domains/input.zig index 965b395a..a3e43d40 100644 --- a/src/cdp/domains/input.zig +++ b/src/cdp/domains/input.zig @@ -17,8 +17,9 @@ // along with this program. If not, see . const std = @import("std"); +const CDP = @import("../CDP.zig"); -pub fn processMessage(cmd: anytype) !void { +pub fn processMessage(cmd: *CDP.Command) !void { const action = std.meta.stringToEnum(enum { dispatchKeyEvent, dispatchMouseEvent, @@ -33,7 +34,7 @@ pub fn processMessage(cmd: anytype) !void { } // https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchKeyEvent -fn dispatchKeyEvent(cmd: anytype) !void { +fn dispatchKeyEvent(cmd: *CDP.Command) !void { const params = (try cmd.params(struct { type: Type, key: []const u8 = "", @@ -74,7 +75,7 @@ fn dispatchKeyEvent(cmd: anytype) !void { } // https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchMouseEvent -fn dispatchMouseEvent(cmd: anytype) !void { +fn dispatchMouseEvent(cmd: *CDP.Command) !void { const params = (try cmd.params(struct { x: f64, y: f64, @@ -104,7 +105,7 @@ fn dispatchMouseEvent(cmd: anytype) !void { } // https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-insertText -fn insertText(cmd: anytype) !void { +fn insertText(cmd: *CDP.Command) !void { const params = (try cmd.params(struct { text: []const u8, // The text to insert })) orelse return error.InvalidParams; diff --git a/src/cdp/domains/inspector.zig b/src/cdp/domains/inspector.zig index b8dea574..40088bce 100644 --- a/src/cdp/domains/inspector.zig +++ b/src/cdp/domains/inspector.zig @@ -17,8 +17,9 @@ // along with this program. If not, see . const std = @import("std"); +const CDP = @import("../CDP.zig"); -pub fn processMessage(cmd: anytype) !void { +pub fn processMessage(cmd: *CDP.Command) !void { const action = std.meta.stringToEnum(enum { enable, disable, diff --git a/src/cdp/domains/log.zig b/src/cdp/domains/log.zig index ed6c0e68..5b5280ef 100644 --- a/src/cdp/domains/log.zig +++ b/src/cdp/domains/log.zig @@ -17,8 +17,9 @@ // along with this program. If not, see . const std = @import("std"); +const CDP = @import("../CDP.zig"); -pub fn processMessage(cmd: anytype) !void { +pub fn processMessage(cmd: *CDP.Command) !void { const action = std.meta.stringToEnum(enum { enable, disable, diff --git a/src/cdp/domains/lp.zig b/src/cdp/domains/lp.zig index 72eb6ee8..fbfde571 100644 --- a/src/cdp/domains/lp.zig +++ b/src/cdp/domains/lp.zig @@ -18,14 +18,18 @@ const std = @import("std"); const lp = @import("lightpanda"); + +const CDP = @import("../CDP.zig"); +const Node = @import("../Node.zig"); + +const DOMNode = @import("../../browser/webapi/Node.zig"); + const markdown = lp.markdown; const SemanticTree = lp.SemanticTree; const interactive = lp.interactive; const structured_data = lp.structured_data; -const Node = @import("../Node.zig"); -const DOMNode = @import("../../browser/webapi/Node.zig"); -pub fn processMessage(cmd: anytype) !void { +pub fn processMessage(cmd: *CDP.Command) !void { const action = std.meta.stringToEnum(enum { getMarkdown, getSemanticTree, @@ -51,7 +55,7 @@ pub fn processMessage(cmd: anytype) !void { } } -fn getSemanticTree(cmd: anytype) !void { +fn getSemanticTree(cmd: *CDP.Command) !void { const Params = struct { format: ?enum { text } = null, prune: ?bool = null, @@ -96,7 +100,7 @@ fn getSemanticTree(cmd: anytype) !void { }, .{}); } -fn getMarkdown(cmd: anytype) !void { +fn getMarkdown(cmd: *CDP.Command) !void { const Params = struct { nodeId: ?Node.Id = null, }; @@ -119,7 +123,7 @@ fn getMarkdown(cmd: anytype) !void { }, .{}); } -fn getInteractiveElements(cmd: anytype) !void { +fn getInteractiveElements(cmd: *CDP.Command) !void { const Params = struct { nodeId: ?Node.Id = null, }; @@ -141,7 +145,7 @@ fn getInteractiveElements(cmd: anytype) !void { }, .{}); } -fn getStructuredData(cmd: anytype) !void { +fn getStructuredData(cmd: *CDP.Command) !void { const bc = cmd.browser_context orelse return error.NoBrowserContext; const page = bc.session.currentPage() orelse return error.PageNotLoaded; @@ -156,7 +160,7 @@ fn getStructuredData(cmd: anytype) !void { }, .{}); } -fn detectForms(cmd: anytype) !void { +fn detectForms(cmd: *CDP.Command) !void { const bc = cmd.browser_context orelse return error.NoBrowserContext; const page = bc.session.currentPage() orelse return error.PageNotLoaded; @@ -173,7 +177,7 @@ fn detectForms(cmd: anytype) !void { }, .{}); } -fn clickNode(cmd: anytype) !void { +fn clickNode(cmd: *CDP.Command) !void { const Params = struct { nodeId: ?Node.Id = null, backendNodeId: ?Node.Id = null, @@ -194,7 +198,7 @@ fn clickNode(cmd: anytype) !void { return cmd.sendResult(.{}, .{}); } -fn fillNode(cmd: anytype) !void { +fn fillNode(cmd: *CDP.Command) !void { const Params = struct { nodeId: ?Node.Id = null, backendNodeId: ?Node.Id = null, @@ -216,7 +220,7 @@ fn fillNode(cmd: anytype) !void { return cmd.sendResult(.{}, .{}); } -fn scrollNode(cmd: anytype) !void { +fn scrollNode(cmd: *CDP.Command) !void { const Params = struct { nodeId: ?Node.Id = null, backendNodeId: ?Node.Id = null, @@ -244,7 +248,7 @@ fn scrollNode(cmd: anytype) !void { return cmd.sendResult(.{}, .{}); } -fn waitForSelector(cmd: anytype) !void { +fn waitForSelector(cmd: *CDP.Command) !void { const Params = struct { selector: []const u8, timeout: ?u32 = null, diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig index a8db008c..575c711f 100644 --- a/src/cdp/domains/network.zig +++ b/src/cdp/domains/network.zig @@ -18,18 +18,21 @@ const std = @import("std"); const lp = @import("lightpanda"); -const Allocator = std.mem.Allocator; + const log = @import("../../log.zig"); -const CdpStorage = @import("storage.zig"); - const id = @import("../id.zig"); +const CDP = @import("../CDP.zig"); + const URL = @import("../../browser/URL.zig"); const Transfer = @import("../../browser/HttpClient.zig").Transfer; const Notification = @import("../../Notification.zig"); const Mime = @import("../../browser/Mime.zig"); -pub fn processMessage(cmd: anytype) !void { +const CdpStorage = @import("storage.zig"); +const Allocator = std.mem.Allocator; + +pub fn processMessage(cmd: *CDP.Command) !void { const action = std.meta.stringToEnum(enum { enable, disable, @@ -59,19 +62,19 @@ pub fn processMessage(cmd: anytype) !void { } } -fn enable(cmd: anytype) !void { +fn enable(cmd: *CDP.Command) !void { const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; try bc.networkEnable(); return cmd.sendResult(null, .{}); } -fn disable(cmd: anytype) !void { +fn disable(cmd: *CDP.Command) !void { const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; bc.networkDisable(); return cmd.sendResult(null, .{}); } -fn setExtraHTTPHeaders(cmd: anytype) !void { +fn setExtraHTTPHeaders(cmd: *CDP.Command) !void { const params = (try cmd.params(struct { headers: std.json.ArrayHashMap([]const u8), })) orelse return error.InvalidParams; @@ -110,7 +113,7 @@ fn cookieMatches(cookie: *const Cookie, name: []const u8, domain: ?[]const u8, p return true; } -fn deleteCookies(cmd: anytype) !void { +fn deleteCookies(cmd: *CDP.Command) !void { const params = (try cmd.params(struct { name: []const u8, url: ?[:0]const u8 = null, @@ -144,14 +147,14 @@ fn deleteCookies(cmd: anytype) !void { return cmd.sendResult(null, .{}); } -fn clearBrowserCookies(cmd: anytype) !void { +fn clearBrowserCookies(cmd: *CDP.Command) !void { if (try cmd.params(struct {}) != null) return error.InvalidParams; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; bc.session.cookie_jar.clearRetainingCapacity(); return cmd.sendResult(null, .{}); } -fn setCookie(cmd: anytype) !void { +fn setCookie(cmd: *CDP.Command) !void { const params = (try cmd.params( CdpStorage.CdpCookie, )) orelse return error.InvalidParams; @@ -162,7 +165,7 @@ fn setCookie(cmd: anytype) !void { try cmd.sendResult(.{ .success = true }, .{}); } -fn setCookies(cmd: anytype) !void { +fn setCookies(cmd: *CDP.Command) !void { const params = (try cmd.params(struct { cookies: []const CdpStorage.CdpCookie, })) orelse return error.InvalidParams; @@ -178,7 +181,7 @@ fn setCookies(cmd: anytype) !void { const GetCookiesParam = struct { urls: ?[]const [:0]const u8 = null, }; -fn getCookies(cmd: anytype) !void { +fn getCookies(cmd: *CDP.Command) !void { const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const params = (try cmd.params(GetCookiesParam)) orelse GetCookiesParam{}; @@ -201,7 +204,7 @@ fn getCookies(cmd: anytype) !void { try cmd.sendResult(.{ .cookies = writer }, .{}); } -fn getResponseBody(cmd: anytype) !void { +fn getResponseBody(cmd: *CDP.Command) !void { const params = (try cmd.params(struct { requestId: []const u8, // "REQ-{d}" })) orelse return error.InvalidParams; @@ -227,7 +230,7 @@ fn getResponseBody(cmd: anytype) !void { }, .{}); } -pub fn httpRequestFail(bc: anytype, msg: *const Notification.RequestFail) !void { +pub fn httpRequestFail(bc: *CDP.BrowserContext, msg: *const Notification.RequestFail) !void { // It's possible that the request failed because we aborted when the client // sent Target.closeTarget. In that case, bc.session_id will be cleared // already, and we can skip sending these messages to the client. @@ -247,7 +250,7 @@ pub fn httpRequestFail(bc: anytype, msg: *const Notification.RequestFail) !void }, .{ .session_id = session_id }); } -pub fn httpRequestStart(bc: anytype, msg: *const Notification.RequestStart) !void { +pub fn httpRequestStart(bc: *CDP.BrowserContext, msg: *const Notification.RequestStart) !void { // detachTarget could be called, in which case, we still have a page doing // things, but no session. const session_id = bc.session_id orelse return; @@ -276,7 +279,7 @@ pub fn httpRequestStart(bc: anytype, msg: *const Notification.RequestStart) !voi }, .{ .session_id = session_id }); } -pub fn httpResponseHeaderDone(arena: Allocator, bc: anytype, msg: *const Notification.ResponseHeaderDone) !void { +pub fn httpResponseHeaderDone(arena: Allocator, bc: *CDP.BrowserContext, msg: *const Notification.ResponseHeaderDone) !void { // detachTarget could be called, in which case, we still have a page doing // things, but no session. const session_id = bc.session_id orelse return; @@ -293,7 +296,7 @@ pub fn httpResponseHeaderDone(arena: Allocator, bc: anytype, msg: *const Notific }, .{ .session_id = session_id }); } -pub fn httpRequestDone(bc: anytype, msg: *const Notification.RequestDone) !void { +pub fn httpRequestDone(bc: *CDP.BrowserContext, msg: *const Notification.RequestDone) !void { // detachTarget could be called, in which case, we still have a page doing // things, but no session. const session_id = bc.session_id orelse return; diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index 108d1e3b..ab922bf3 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -1,4 +1,5 @@ // Copyright (C) 2023-2025 Lightpanda (Selecy SAS) + // // Francis Bouvier // Pierre Tachoire @@ -22,6 +23,8 @@ const lp = @import("lightpanda"); const screenshot_png = @embedFile("screenshot.png"); const id = @import("../id.zig"); +const CDP = @import("../CDP.zig"); + const log = @import("../../log.zig"); const js = @import("../../browser/js/js.zig"); const URL = @import("../../browser/URL.zig"); @@ -31,7 +34,7 @@ const Notification = @import("../../Notification.zig"); const Allocator = std.mem.Allocator; -pub fn processMessage(cmd: anytype) !void { +pub fn processMessage(cmd: *CDP.Command) !void { const action = std.meta.stringToEnum(enum { enable, getFrameTree, @@ -78,7 +81,7 @@ const Frame = struct { gatedAPIFeatures: [][]const u8 = &[0][]const u8{}, }; -fn getFrameTree(cmd: anytype) !void { +fn getFrameTree(cmd: *CDP.Command) !void { // Stagehand parses the response and error if we don't return a // correct one for this call when browser context or target id are missing. const startup = .{ @@ -108,7 +111,7 @@ fn getFrameTree(cmd: anytype) !void { }, .{}); } -fn setLifecycleEventsEnabled(cmd: anytype) !void { +fn setLifecycleEventsEnabled(cmd: *CDP.Command) !void { const params = (try cmd.params(struct { enabled: bool, })) orelse return error.InvalidParams; @@ -149,7 +152,7 @@ fn setLifecycleEventsEnabled(cmd: anytype) !void { return cmd.sendResult(null, .{}); } -fn addScriptToEvaluateOnNewDocument(cmd: anytype) !void { +fn addScriptToEvaluateOnNewDocument(cmd: *CDP.Command) !void { const params = (try cmd.params(struct { source: []const u8, worldName: ?[]const u8 = null, @@ -179,7 +182,7 @@ fn addScriptToEvaluateOnNewDocument(cmd: anytype) !void { }, .{}); } -fn removeScriptToEvaluateOnNewDocument(cmd: anytype) !void { +fn removeScriptToEvaluateOnNewDocument(cmd: *CDP.Command) !void { const params = (try cmd.params(struct { identifier: []const u8, })) orelse return error.InvalidParams; @@ -198,7 +201,7 @@ fn removeScriptToEvaluateOnNewDocument(cmd: anytype) !void { return cmd.sendResult(null, .{}); } -fn close(cmd: anytype) !void { +fn close(cmd: *CDP.Command) !void { const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const target_id = bc.target_id orelse return error.TargetNotLoaded; @@ -235,7 +238,7 @@ fn close(cmd: anytype) !void { bc.target_id = null; } -fn createIsolatedWorld(cmd: anytype) !void { +fn createIsolatedWorld(cmd: *CDP.Command) !void { const params = (try cmd.params(struct { frameId: []const u8, worldName: []const u8, @@ -255,7 +258,7 @@ fn createIsolatedWorld(cmd: anytype) !void { return cmd.sendResult(.{ .executionContextId = js_context.id }, .{}); } -fn navigate(cmd: anytype) !void { +fn navigate(cmd: *CDP.Command) !void { const params = (try cmd.params(struct { url: [:0]const u8, // referrer: ?[]const u8 = null, @@ -289,7 +292,7 @@ fn navigate(cmd: anytype) !void { }); } -fn doReload(cmd: anytype) !void { +fn doReload(cmd: *CDP.Command) !void { const params = try cmd.params(struct { ignoreCache: ?bool = null, scriptToEvaluateOnLoad: ?[]const u8 = null, @@ -319,7 +322,7 @@ fn doReload(cmd: anytype) !void { }); } -pub fn pageNavigate(bc: anytype, event: *const Notification.PageNavigate) !void { +pub fn pageNavigate(bc: *CDP.BrowserContext, event: *const Notification.PageNavigate) !void { // detachTarget could be called, in which case, we still have a page doing // things, but no session. const session_id = bc.session_id orelse return; @@ -371,7 +374,7 @@ pub fn pageNavigate(bc: anytype, event: *const Notification.PageNavigate) !void }, .{ .session_id = session_id }); } -pub fn pageRemove(bc: anytype) !void { +pub fn pageRemove(bc: *CDP.BrowserContext) !void { // Clear all remote object mappings to prevent stale objectIds from being used // after the context is destroy bc.inspector_session.inspector.resetContextGroup(); @@ -382,7 +385,7 @@ pub fn pageRemove(bc: anytype) !void { } } -pub fn pageCreated(bc: anytype, page: *Page) !void { +pub fn pageCreated(bc: *CDP.BrowserContext, page: *Page) !void { _ = bc.cdp.page_arena.reset(.{ .retain_with_limit = 1024 * 512 }); for (bc.isolated_worlds.items) |isolated_world| { @@ -394,7 +397,7 @@ pub fn pageCreated(bc: anytype, page: *Page) !void { bc.captured_responses = .empty; } -pub fn pageFrameCreated(bc: anytype, event: *const Notification.PageFrameCreated) !void { +pub fn pageFrameCreated(bc: *CDP.BrowserContext, event: *const Notification.PageFrameCreated) !void { const session_id = bc.session_id orelse return; const cdp = bc.cdp; @@ -415,7 +418,7 @@ pub fn pageFrameCreated(bc: anytype, event: *const Notification.PageFrameCreated } } -pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.PageNavigated) !void { +pub fn pageNavigated(arena: Allocator, bc: *CDP.BrowserContext, event: *const Notification.PageNavigated) !void { // detachTarget could be called, in which case, we still have a page doing // things, but no session. const session_id = bc.session_id orelse return; @@ -597,15 +600,15 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P }, .{ .session_id = session_id }); } -pub fn pageNetworkIdle(bc: anytype, event: *const Notification.PageNetworkIdle) !void { +pub fn pageNetworkIdle(bc: *CDP.BrowserContext, event: *const Notification.PageNetworkIdle) !void { return sendPageLifecycle(bc, "networkIdle", event.timestamp, &id.toFrameId(event.frame_id), &id.toLoaderId(event.req_id)); } -pub fn pageNetworkAlmostIdle(bc: anytype, event: *const Notification.PageNetworkAlmostIdle) !void { +pub fn pageNetworkAlmostIdle(bc: *CDP.BrowserContext, event: *const Notification.PageNetworkAlmostIdle) !void { return sendPageLifecycle(bc, "networkAlmostIdle", event.timestamp, &id.toFrameId(event.frame_id), &id.toLoaderId(event.req_id)); } -fn sendPageLifecycle(bc: anytype, name: []const u8, timestamp: u64, frame_id: []const u8, loader_id: []const u8) !void { +fn sendPageLifecycle(bc: *CDP.BrowserContext, name: []const u8, timestamp: u64, frame_id: []const u8, loader_id: []const u8) !void { // detachTarget could be called, in which case, we still have a page doing // things, but no session. const session_id = bc.session_id orelse return; @@ -640,7 +643,7 @@ fn base64Encode(comptime input: []const u8) [std.base64.standard.Encoder.calcSiz return buf; } -fn captureScreenshot(cmd: anytype) !void { +fn captureScreenshot(cmd: *CDP.Command) !void { const Params = struct { format: ?[]const u8 = "png", quality: ?u8 = null, @@ -676,7 +679,7 @@ fn captureScreenshot(cmd: anytype) !void { }, .{}); } -fn getLayoutMetrics(cmd: anytype) !void { +fn getLayoutMetrics(cmd: *CDP.Command) !void { const width = 1920; const height = 1080; diff --git a/src/cdp/domains/performance.zig b/src/cdp/domains/performance.zig index b8dea574..40088bce 100644 --- a/src/cdp/domains/performance.zig +++ b/src/cdp/domains/performance.zig @@ -17,8 +17,9 @@ // along with this program. If not, see . const std = @import("std"); +const CDP = @import("../CDP.zig"); -pub fn processMessage(cmd: anytype) !void { +pub fn processMessage(cmd: *CDP.Command) !void { const action = std.meta.stringToEnum(enum { enable, disable, diff --git a/src/cdp/domains/runtime.zig b/src/cdp/domains/runtime.zig index 138155db..96b142af 100644 --- a/src/cdp/domains/runtime.zig +++ b/src/cdp/domains/runtime.zig @@ -19,7 +19,9 @@ const std = @import("std"); const builtin = @import("builtin"); -pub fn processMessage(cmd: anytype) !void { +const CDP = @import("../CDP.zig"); + +pub fn processMessage(cmd: *CDP.Command) !void { const action = std.meta.stringToEnum(enum { enable, runIfWaitingForDebugger, @@ -36,7 +38,7 @@ pub fn processMessage(cmd: anytype) !void { } } -fn sendInspector(cmd: anytype, action: anytype) !void { +fn sendInspector(cmd: *CDP.Command, action: anytype) !void { // save script in file at debug mode if (builtin.mode == .Debug) { try logInspector(cmd, action); @@ -48,7 +50,7 @@ fn sendInspector(cmd: anytype, action: anytype) !void { bc.callInspector(cmd.input.json); } -fn logInspector(cmd: anytype, action: anytype) !void { +fn logInspector(cmd: *CDP.Command, action: anytype) !void { const script = switch (action) { .evaluate => blk: { const params = (try cmd.params(struct { diff --git a/src/cdp/domains/security.zig b/src/cdp/domains/security.zig index 4e6ff663..ecc3fbf0 100644 --- a/src/cdp/domains/security.zig +++ b/src/cdp/domains/security.zig @@ -17,8 +17,9 @@ // along with this program. If not, see . const std = @import("std"); +const CDP = @import("../CDP.zig"); -pub fn processMessage(cmd: anytype) !void { +pub fn processMessage(cmd: *CDP.Command) !void { const action = std.meta.stringToEnum(enum { enable, disable, @@ -32,7 +33,7 @@ pub fn processMessage(cmd: anytype) !void { } } -fn setIgnoreCertificateErrors(cmd: anytype) !void { +fn setIgnoreCertificateErrors(cmd: *CDP.Command) !void { const params = (try cmd.params(struct { ignore: bool, })) orelse return error.InvalidParams; diff --git a/src/cdp/domains/storage.zig b/src/cdp/domains/storage.zig index bd2e24c0..a1c0aace 100644 --- a/src/cdp/domains/storage.zig +++ b/src/cdp/domains/storage.zig @@ -18,13 +18,16 @@ const std = @import("std"); +const CDP = @import("../CDP.zig"); + const log = @import("../../log.zig"); const URL = @import("../../browser/URL.zig"); const Cookie = @import("../../browser/webapi/storage/storage.zig").Cookie; + const CookieJar = Cookie.Jar; pub const PreparedUri = Cookie.PreparedUri; -pub fn processMessage(cmd: anytype) !void { +pub fn processMessage(cmd: *CDP.Command) !void { const action = std.meta.stringToEnum(enum { clearCookies, setCookies, @@ -40,7 +43,7 @@ pub fn processMessage(cmd: anytype) !void { const BrowserContextParam = struct { browserContextId: ?[]const u8 = null }; -fn clearCookies(cmd: anytype) !void { +fn clearCookies(cmd: *CDP.Command) !void { const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const params = (try cmd.params(BrowserContextParam)) orelse BrowserContextParam{}; @@ -55,7 +58,7 @@ fn clearCookies(cmd: anytype) !void { return cmd.sendResult(null, .{}); } -fn getCookies(cmd: anytype) !void { +fn getCookies(cmd: *CDP.Command) !void { const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const params = (try cmd.params(BrowserContextParam)) orelse BrowserContextParam{}; @@ -69,7 +72,7 @@ fn getCookies(cmd: anytype) !void { try cmd.sendResult(.{ .cookies = writer }, .{}); } -fn setCookies(cmd: anytype) !void { +fn setCookies(cmd: *CDP.Command) !void { const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const params = (try cmd.params(struct { cookies: []const CdpCookie, diff --git a/src/cdp/domains/target.zig b/src/cdp/domains/target.zig index a26c8d78..fa03dd9d 100644 --- a/src/cdp/domains/target.zig +++ b/src/cdp/domains/target.zig @@ -20,11 +20,13 @@ const std = @import("std"); const lp = @import("lightpanda"); const id = @import("../id.zig"); +const CDP = @import("../CDP.zig"); + const log = @import("../../log.zig"); const URL = @import("../../browser/URL.zig"); const js = @import("../../browser/js/js.zig"); -pub fn processMessage(cmd: anytype) !void { +pub fn processMessage(cmd: *CDP.Command) !void { const action = std.meta.stringToEnum(enum { getTargets, attachToTarget, @@ -60,7 +62,7 @@ pub fn processMessage(cmd: anytype) !void { } } -fn getTargets(cmd: anytype) !void { +fn getTargets(cmd: *CDP.Command) !void { // If no context available, return an empty array. const bc = cmd.browser_context orelse { return cmd.sendResult(.{ @@ -86,7 +88,7 @@ fn getTargets(cmd: anytype) !void { }, .{ .include_session_id = false }); } -fn getBrowserContexts(cmd: anytype) !void { +fn getBrowserContexts(cmd: *CDP.Command) !void { var browser_context_ids: []const []const u8 = undefined; if (cmd.browser_context) |bc| { browser_context_ids = &.{bc.id}; @@ -99,7 +101,7 @@ fn getBrowserContexts(cmd: anytype) !void { }, .{ .include_session_id = false }); } -fn createBrowserContext(cmd: anytype) !void { +fn createBrowserContext(cmd: *CDP.Command) !void { const params = try cmd.params(struct { disposeOnDetach: bool = false, proxyServer: ?[:0]const u8 = null, @@ -130,7 +132,7 @@ fn createBrowserContext(cmd: anytype) !void { }, .{}); } -fn disposeBrowserContext(cmd: anytype) !void { +fn disposeBrowserContext(cmd: *CDP.Command) !void { const params = (try cmd.params(struct { browserContextId: []const u8, })) orelse return error.InvalidParams; @@ -141,7 +143,7 @@ fn disposeBrowserContext(cmd: anytype) !void { try cmd.sendResult(null, .{}); } -fn createTarget(cmd: anytype) !void { +fn createTarget(cmd: *CDP.Command) !void { const params = (try cmd.params(struct { url: [:0]const u8 = "about:blank", // width: ?u64 = null, @@ -230,7 +232,7 @@ fn createTarget(cmd: anytype) !void { }, .{}); } -fn attachToTarget(cmd: anytype) !void { +fn attachToTarget(cmd: *CDP.Command) !void { const params = (try cmd.params(struct { targetId: []const u8, flatten: bool = true, @@ -247,7 +249,7 @@ fn attachToTarget(cmd: anytype) !void { return cmd.sendResult(.{ .sessionId = bc.session_id }, .{}); } -fn attachToBrowserTarget(cmd: anytype) !void { +fn attachToBrowserTarget(cmd: *CDP.Command) !void { const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const session_id = bc.session_id orelse cmd.cdp.session_id_gen.next(); @@ -269,7 +271,7 @@ fn attachToBrowserTarget(cmd: anytype) !void { return cmd.sendResult(.{ .sessionId = bc.session_id }, .{}); } -fn closeTarget(cmd: anytype) !void { +fn closeTarget(cmd: *CDP.Command) !void { const params = (try cmd.params(struct { targetId: []const u8, })) orelse return error.InvalidParams; @@ -310,7 +312,7 @@ fn closeTarget(cmd: anytype) !void { bc.target_id = null; } -fn getTargetInfo(cmd: anytype) !void { +fn getTargetInfo(cmd: *CDP.Command) !void { const Params = struct { targetId: ?[]const u8 = null, }; @@ -347,7 +349,7 @@ fn getTargetInfo(cmd: anytype) !void { }, .{ .include_session_id = false }); } -fn sendMessageToTarget(cmd: anytype) !void { +fn sendMessageToTarget(cmd: *CDP.Command) !void { const params = (try cmd.params(struct { message: []const u8, sessionId: []const u8, @@ -365,32 +367,19 @@ fn sendMessageToTarget(cmd: anytype) !void { return error.UnknownSessionId; } - const Capture = struct { - aw: std.Io.Writer.Allocating, - - pub fn sendJSON(self: *@This(), message: anytype) !void { - return std.json.Stringify.value(message, .{ - .emit_null_optional_fields = false, - }, &self.aw.writer); - } - }; - - var capture = Capture{ - .aw = .init(cmd.arena), - }; - - cmd.cdp.dispatch(cmd.arena, &capture, params.message) catch |err| { + var aw = std.Io.Writer.Allocating.init(cmd.arena); + cmd.cdp.dispatch(cmd.arena, .{ .capture = &aw.writer }, params.message) catch |err| { log.err(.cdp, "internal dispatch error", .{ .err = err, .id = cmd.input.id, .message = params.message }); return err; }; try cmd.sendEvent("Target.receivedMessageFromTarget", .{ - .message = capture.aw.written(), + .message = aw.written(), .sessionId = params.sessionId, }, .{}); } -fn detachFromTarget(cmd: anytype) !void { +fn detachFromTarget(cmd: *CDP.Command) !void { if (cmd.browser_context) |bc| { if (bc.session_id) |session_id| { try cmd.sendEvent("Target.detachedFromTarget", .{ @@ -404,11 +393,11 @@ fn detachFromTarget(cmd: anytype) !void { } // TODO: noop method -fn setDiscoverTargets(cmd: anytype) !void { +fn setDiscoverTargets(cmd: *CDP.Command) !void { return cmd.sendResult(null, .{}); } -fn setAutoAttach(cmd: anytype) !void { +fn setAutoAttach(cmd: *CDP.Command) !void { const params = (try cmd.params(struct { autoAttach: bool, waitForDebuggerOnStart: bool, @@ -468,7 +457,7 @@ fn setAutoAttach(cmd: anytype) !void { try cmd.sendResult(null, .{}); } -fn doAttachtoTarget(cmd: anytype, target_id: []const u8) !void { +fn doAttachtoTarget(cmd: *CDP.Command, target_id: []const u8) !void { const bc = cmd.browser_context.?; const session_id = bc.session_id orelse cmd.cdp.session_id_gen.next(); diff --git a/src/cdp/testing.zig b/src/cdp/testing.zig index 0edba1d4..a7c7317b 100644 --- a/src/cdp/testing.zig +++ b/src/cdp/testing.zig @@ -64,7 +64,7 @@ const TestContext = struct { session_id: ?[]const u8 = null, url: ?[:0]const u8 = null, }; - pub fn loadBrowserContext(self: *TestContext, opts: BrowserContextOpts) !*CDP.BrowserContext(CDP) { + pub fn loadBrowserContext(self: *TestContext, opts: BrowserContextOpts) !*CDP.BrowserContext { var c = self.cdp(); if (c.browser_context) |bc| { _ = c.disposeBrowserContext(bc.id);