diff --git a/src/Notification.zig b/src/Notification.zig index 11d9c74d..91fcb673 100644 --- a/src/Notification.zig +++ b/src/Notification.zig @@ -102,24 +102,30 @@ const EventType = std.meta.FieldEnum(Events); pub const PageRemove = struct {}; pub const PageNavigate = struct { - req_id: usize, + req_id: u32, + page_id: u32, timestamp: u64, url: [:0]const u8, opts: Page.NavigateOpts, }; pub const PageNavigated = struct { - req_id: usize, + req_id: u32, + page_id: u32, timestamp: u64, url: [:0]const u8, opts: Page.NavigatedOpts, }; pub const PageNetworkIdle = struct { + req_id: u32, + page_id: u32, timestamp: u64, }; pub const PageNetworkAlmostIdle = struct { + req_id: u32, + page_id: u32, timestamp: u64, }; @@ -305,6 +311,7 @@ test "Notification" { // noop notifier.dispatch(.page_navigate, &.{ + .page_id = 0, .req_id = 1, .timestamp = 4, .url = undefined, @@ -315,6 +322,7 @@ test "Notification" { try notifier.register(.page_navigate, &tc, TestClient.pageNavigate); notifier.dispatch(.page_navigate, &.{ + .page_id = 0, .req_id = 1, .timestamp = 4, .url = undefined, @@ -324,6 +332,7 @@ test "Notification" { notifier.unregisterAll(&tc); notifier.dispatch(.page_navigate, &.{ + .page_id = 0, .req_id = 1, .timestamp = 10, .url = undefined, @@ -334,23 +343,25 @@ test "Notification" { try notifier.register(.page_navigate, &tc, TestClient.pageNavigate); try notifier.register(.page_navigated, &tc, TestClient.pageNavigated); notifier.dispatch(.page_navigate, &.{ + .page_id = 0, .req_id = 1, .timestamp = 10, .url = undefined, .opts = .{}, }); - notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 6, .url = undefined, .opts = .{} }); + notifier.dispatch(.page_navigated, &.{ .page_id = 0, .req_id = 1, .timestamp = 6, .url = undefined, .opts = .{} }); try testing.expectEqual(14, tc.page_navigate); try testing.expectEqual(6, tc.page_navigated); notifier.unregisterAll(&tc); notifier.dispatch(.page_navigate, &.{ + .page_id = 0, .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{}, }); - notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} }); + notifier.dispatch(.page_navigated, &.{ .page_id = 0, .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} }); try testing.expectEqual(14, tc.page_navigate); try testing.expectEqual(6, tc.page_navigated); @@ -358,27 +369,27 @@ test "Notification" { // unregister try notifier.register(.page_navigate, &tc, TestClient.pageNavigate); try notifier.register(.page_navigated, &tc, TestClient.pageNavigated); - notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} }); - notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} }); + notifier.dispatch(.page_navigate, &.{ .page_id = 0, .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} }); + notifier.dispatch(.page_navigated, &.{ .page_id = 0, .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} }); try testing.expectEqual(114, tc.page_navigate); try testing.expectEqual(1006, tc.page_navigated); notifier.unregister(.page_navigate, &tc); - notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} }); - notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} }); + notifier.dispatch(.page_navigate, &.{ .page_id = 0, .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} }); + notifier.dispatch(.page_navigated, &.{ .page_id = 0, .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} }); try testing.expectEqual(114, tc.page_navigate); try testing.expectEqual(2006, tc.page_navigated); notifier.unregister(.page_navigated, &tc); - notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} }); - notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} }); + notifier.dispatch(.page_navigate, &.{ .page_id = 0, .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} }); + notifier.dispatch(.page_navigated, &.{ .page_id = 0, .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} }); try testing.expectEqual(114, tc.page_navigate); try testing.expectEqual(2006, tc.page_navigated); // already unregistered, try anyways notifier.unregister(.page_navigated, &tc); - notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} }); - notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} }); + notifier.dispatch(.page_navigate, &.{ .page_id = 0, .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} }); + notifier.dispatch(.page_navigated, &.{ .page_id = 0, .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} }); try testing.expectEqual(114, tc.page_navigate); try testing.expectEqual(2006, tc.page_navigated); } diff --git a/src/browser/Page.zig b/src/browser/Page.zig index a1433fbb..a562c65b 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -79,6 +79,10 @@ pub const BUF_SIZE = 1024; const Page = @This(); +// This is the "id" of the frame. It can be re-used from page-to-page, e.g. +// when navigating. +id: u32, + _session: *Session, _event_manager: EventManager, @@ -218,10 +222,10 @@ document: *Document, // DOM version used to invalidate cached state of "live" collections version: usize = 0, -_req_id: ?usize = null, +_req_id: u32 = 0, _navigated_options: ?NavigatedOpts = null, -pub fn init(self: *Page, session: *Session) !void { +pub fn init(self: *Page, id: u32, session: *Session) !void { if (comptime IS_DEBUG) { log.debug(.page, "page.init", .{}); } @@ -240,6 +244,7 @@ pub fn init(self: *Page, session: *Session) !void { })).asDocument(); self.* = .{ + .id = id, .js = undefined, .arena = page_arena, .document = document, @@ -413,7 +418,8 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi // This assumption may be false when CDP Page.addScriptToEvaluateOnNewDocument is implemented self.documentIsComplete(); - self._session.notification.dispatch(.page_navigate, &.{ + session.notification.dispatch(.page_navigate, &.{ + .page_id = self.id, .req_id = req_id, .opts = opts, .url = request_url, @@ -421,14 +427,15 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi }); // Record telemetry for navigation - self._session.browser.app.telemetry.record(.{ + session.browser.app.telemetry.record(.{ .navigate = .{ .tls = false, // about:blank is not TLS - .proxy = self._session.browser.app.config.httpProxy() != null, + .proxy = session.browser.app.config.httpProxy() != null, }, }); - self._session.notification.dispatch(.page_navigated, &.{ + session.notification.dispatch(.page_navigated, &.{ + .page_id = self.id, .req_id = req_id, .opts = .{ .cdp_id = opts.cdp_id, @@ -440,11 +447,11 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi }); // force next request id manually b/c we won't create a real req. - _ = self._session.browser.http_client.incrReqId(); + _ = session.browser.http_client.incrReqId(); return; } - var http_client = self._session.browser.http_client; + var http_client = session.browser.http_client; self.url = try self.arena.dupeZ(u8, request_url); @@ -463,7 +470,8 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi // We dispatch page_navigate event before sending the request. // It ensures the event page_navigated is not dispatched before this one. - self._session.notification.dispatch(.page_navigate, &.{ + session.notification.dispatch(.page_navigate, &.{ + .page_id = self.id, .req_id = req_id, .opts = opts, .url = self.url, @@ -471,9 +479,9 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi }); // Record telemetry for navigation - self._session.browser.app.telemetry.record(.{ .navigate = .{ + session.browser.app.telemetry.record(.{ .navigate = .{ .tls = std.ascii.startsWithIgnoreCase(self.url, "https://"), - .proxy = self._session.browser.app.config.httpProxy() != null, + .proxy = session.browser.app.config.httpProxy() != null, } }); session.navigation._current_navigation_kind = opts.kind; @@ -481,10 +489,11 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi http_client.request(.{ .ctx = self, .url = self.url, + .page_id = self.id, .method = opts.method, .headers = headers, .body = opts.body, - .cookie_jar = &self._session.cookie_jar, + .cookie_jar = &session.cookie_jar, .resource_type = .document, .notification = self._session.notification, .header_callback = pageHeaderDoneCallback, @@ -611,12 +620,12 @@ pub fn documentIsComplete(self: *Page) void { }; if (IS_DEBUG) { - std.debug.assert(self._req_id != null); std.debug.assert(self._navigated_options != null); } self._session.notification.dispatch(.page_navigated, &.{ - .req_id = self._req_id.?, + .page_id = self.id, + .req_id = self._req_id, .opts = self._navigated_options.?, .url = self.url, .timestamp = timestamp(.monotonic), @@ -1422,6 +1431,8 @@ pub fn deliverSlotchangeEvents(self: *Page) void { fn notifyNetworkIdle(self: *Page) void { lp.assert(self._notified_network_idle == .done, "Page.notifyNetworkIdle", .{}); self._session.notification.dispatch(.page_network_idle, &.{ + .page_id = self.id, + .req_id = self._req_id, .timestamp = timestamp(.monotonic), }); } @@ -1429,6 +1440,8 @@ fn notifyNetworkIdle(self: *Page) void { fn notifyNetworkAlmostIdle(self: *Page) void { lp.assert(self._notified_network_almost_idle == .done, "Page.notifyNetworkAlmostIdle", .{}); self._session.notification.dispatch(.page_network_almost_idle, &.{ + .page_id = self.id, + .req_id = self._req_id, .timestamp = timestamp(.monotonic), }); } diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig index 4bfec58a..570ef743 100644 --- a/src/browser/ScriptManager.zig +++ b/src/browser/ScriptManager.zig @@ -259,6 +259,7 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e .url = url, .ctx = script, .method = .GET, + .page_id = page.id, .headers = try self.getHeaders(url), .blocking = is_blocking, .cookie_jar = &page._session.cookie_jar, @@ -358,9 +359,11 @@ pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const .manager = self, }; + const page = self.page; + if (comptime IS_DEBUG) { var ls: js.Local.Scope = undefined; - self.page.js.localScope(&ls); + page.js.localScope(&ls); defer ls.deinit(); log.debug(.http, "script queue", .{ @@ -375,10 +378,11 @@ pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const .url = url, .ctx = script, .method = .GET, + .page_id = page.id, .headers = try self.getHeaders(url), - .cookie_jar = &self.page._session.cookie_jar, + .cookie_jar = &page._session.cookie_jar, .resource_type = .script, - .notification = self.page._session.notification, + .notification = page._session.notification, .start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null, .header_callback = Script.headerCallback, .data_callback = Script.dataCallback, @@ -451,9 +455,10 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C } }, }; + const page = self.page; if (comptime IS_DEBUG) { var ls: js.Local.Scope = undefined; - self.page.js.localScope(&ls); + page.js.localScope(&ls); defer ls.deinit(); log.debug(.http, "script queue", .{ @@ -476,11 +481,12 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C try self.client.request(.{ .url = url, .method = .GET, + .page_id = page.id, .headers = try self.getHeaders(url), .ctx = script, .resource_type = .script, - .cookie_jar = &self.page._session.cookie_jar, - .notification = self.page._session.notification, + .cookie_jar = &page._session.cookie_jar, + .notification = page._session.notification, .start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null, .header_callback = Script.headerCallback, .data_callback = Script.dataCallback, diff --git a/src/browser/Session.zig b/src/browser/Session.zig index aa1f8de7..2d0f87ce 100644 --- a/src/browser/Session.zig +++ b/src/browser/Session.zig @@ -63,6 +63,8 @@ navigation: Navigation, page: ?Page, +page_id_gen: u32, + pub fn init(self: *Session, browser: *Browser, notification: *Notification) !void { const allocator = browser.app.allocator; const arena = try browser.arena_pool.acquire(); @@ -75,6 +77,7 @@ pub fn init(self: *Session, browser: *Browser, notification: *Notification) !voi .page = null, .arena = arena, .history = .{}, + .page_id_gen = 0, // The prototype (EventTarget) for Navigation is created when a Page is created. .navigation = .{ ._proto = undefined }, .storage_shed = .{}, @@ -104,7 +107,11 @@ pub fn createPage(self: *Session) !*Page { self.page = @as(Page, undefined); const page = &self.page.?; - try Page.init(page, self); + + const id = self.page_id_gen +% 1; + self.page_id_gen = id; + + try Page.init(page, id, self); // Creates a new NavigationEventTarget for this page. try self.navigation.onNewPage(page); @@ -140,12 +147,16 @@ pub fn replacePage(self: *Session) !*Page { } lp.assert(self.page != null, "Session.replacePage null page", .{}); - self.page.?.deinit(); + + var current = self.page.?; + const page_id = current.id; + current.deinit(); + self.browser.env.memoryPressureNotification(.moderate); self.page = @as(Page, undefined); const page = &self.page.?; - try Page.init(page, self); + try Page.init(page, page_id, self); return page; } @@ -159,6 +170,11 @@ pub const WaitResult = enum { cdp_socket, }; +pub fn findPage(self: *Session, id: u32) ?*Page { + const page = self.currentPage() orelse return null; + return if (page.id == id) page else null; +} + pub fn wait(self: *Session, wait_ms: u32) WaitResult { while (true) { if (self.page) |*page| { @@ -181,8 +197,9 @@ pub fn wait(self: *Session, wait_ms: u32) WaitResult { fn processScheduledNavigation(self: *Session) !void { defer self.browser.arena_pool.reset(self.transfer_arena, 4 * 1024); - const url, const opts = blk: { - const qn = self.page.?._queued_navigation.?; + const url, const opts, const page_id = blk: { + const page = self.page.?; + const qn = page._queued_navigation.?; // qn might not be safe to use after self.removePage is called, hence // this block; const url = qn.url; @@ -193,7 +210,7 @@ fn processScheduledNavigation(self: *Session) !void { self.browser.http_client.abort(); self.removePage(); - break :blk .{ url, opts }; + break :blk .{ url, opts, page.id }; }; const page = self.createPage() catch |err| { @@ -203,6 +220,7 @@ fn processScheduledNavigation(self: *Session) !void { }); return err; }; + page.id = page_id; page.navigate(url, opts) catch |err| { log.err(.browser, "queued navigation error", .{ .err = err, .url = url }); diff --git a/src/browser/webapi/net/Fetch.zig b/src/browser/webapi/net/Fetch.zig index 2eea37e3..47fa8bc5 100644 --- a/src/browser/webapi/net/Fetch.zig +++ b/src/browser/webapi/net/Fetch.zig @@ -72,6 +72,7 @@ pub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise { try http_client.request(.{ .ctx = fetch, + .page_id = page.id, .url = request._url, .method = request._method, .body = request._body, diff --git a/src/browser/webapi/net/XMLHttpRequest.zig b/src/browser/webapi/net/XMLHttpRequest.zig index 9044eec9..e39e8ce4 100644 --- a/src/browser/webapi/net/XMLHttpRequest.zig +++ b/src/browser/webapi/net/XMLHttpRequest.zig @@ -223,6 +223,7 @@ pub fn send(self: *XMLHttpRequest, body_: ?[]const u8) !void { try http_client.request(.{ .ctx = self, .url = self._url, + .page_id = page.id, .method = self._method, .headers = headers, .body = self._request_body, diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index cb4cce92..f7a3dd88 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -30,12 +30,11 @@ const Browser = @import("../browser/Browser.zig"); const Session = @import("../browser/Session.zig"); const HttpClient = @import("../http/Client.zig"); const Page = @import("../browser/Page.zig"); -const Incrementing = @import("../id.zig").Incrementing; +const Incrementing = @import("id.zig").Incrementing; const Notification = @import("../Notification.zig"); const InterceptState = @import("domains/fetch.zig").InterceptState; pub const URL_BASE = "chrome://newtab/"; -pub const LOADER_ID = "LOADERID24DD2FD56CF1EF33C965C79C"; const IS_DEBUG = @import("builtin").mode == .Debug; @@ -45,7 +44,6 @@ pub const CDP = CDPT(struct { const SessionIdGen = Incrementing(u32, "SID"); const TargetIdGen = Incrementing(u32, "TID"); -const LoaderIdGen = Incrementing(u32, "LID"); const BrowserContextIdGen = Incrementing(u32, "BID"); // Generic so that we can inject mocks into it. @@ -63,7 +61,6 @@ pub fn CDPT(comptime TypeProvider: type) type { target_auto_attach: bool = false, target_id_gen: TargetIdGen = .{}, - loader_id_gen: LoaderIdGen = .{}, session_id_gen: SessionIdGen = .{}, browser_context_id_gen: BrowserContextIdGen = .{}, @@ -200,7 +197,7 @@ pub fn CDPT(comptime TypeProvider: type) type { .frameTree = .{ .frame = .{ .id = "TID-STARTUP-B", - .loaderId = LOADER_ID, + .loaderId = "LOADERID24DD2FD56CF1EF33C965C79C", .securityOrigin = URL_BASE, .url = "about:blank", .secureContextType = "Secure", @@ -350,7 +347,7 @@ pub fn BrowserContext(comptime CDP_T: type) type { // 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: ?[]const u8, + 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 @@ -362,7 +359,6 @@ pub fn BrowserContext(comptime CDP_T: type) type { // we should reject it. session_id: ?[]const u8, - loader_id: []const u8, security_origin: []const u8, page_life_cycle_events: bool, secure_context_type: []const u8, @@ -416,7 +412,6 @@ pub fn BrowserContext(comptime CDP_T: type) type { .session = session, .security_origin = URL_BASE, .secure_context_type = "Secure", // TODO = enum - .loader_id = LOADER_ID, .page_life_cycle_events = false, // TODO; Target based value .node_registry = registry, .node_search_list = undefined, @@ -593,7 +588,7 @@ pub fn BrowserContext(comptime CDP_T: type) type { pub fn onPageNavigate(ctx: *anyopaque, msg: *const Notification.PageNavigate) !void { const self: *Self = @ptrCast(@alignCast(ctx)); defer self.resetNotificationArena(); - return @import("domains/page.zig").pageNavigate(self.notification_arena, self, msg); + return @import("domains/page.zig").pageNavigate(self, msg); } pub fn onPageNavigated(ctx: *anyopaque, msg: *const Notification.PageNavigated) !void { @@ -615,19 +610,19 @@ pub fn BrowserContext(comptime CDP_T: type) type { pub fn onHttpRequestStart(ctx: *anyopaque, msg: *const Notification.RequestStart) !void { const self: *Self = @ptrCast(@alignCast(ctx)); defer self.resetNotificationArena(); - try @import("domains/network.zig").httpRequestStart(self.notification_arena, self, msg); + try @import("domains/network.zig").httpRequestStart(self, msg); } pub fn onHttpRequestIntercept(ctx: *anyopaque, msg: *const Notification.RequestIntercept) !void { const self: *Self = @ptrCast(@alignCast(ctx)); defer self.resetNotificationArena(); - try @import("domains/fetch.zig").requestIntercept(self.notification_arena, self, msg); + try @import("domains/fetch.zig").requestIntercept(self, msg); } pub fn onHttpRequestFail(ctx: *anyopaque, msg: *const Notification.RequestFail) !void { const self: *Self = @ptrCast(@alignCast(ctx)); defer self.resetNotificationArena(); - return @import("domains/network.zig").httpRequestFail(self.notification_arena, self, msg); + return @import("domains/network.zig").httpRequestFail(self, msg); } pub fn onHttpResponseHeadersDone(ctx: *anyopaque, msg: *const Notification.ResponseHeaderDone) !void { @@ -639,7 +634,7 @@ pub fn BrowserContext(comptime CDP_T: type) type { pub fn onHttpRequestDone(ctx: *anyopaque, msg: *const Notification.RequestDone) !void { const self: *Self = @ptrCast(@alignCast(ctx)); defer self.resetNotificationArena(); - return @import("domains/network.zig").httpRequestDone(self.notification_arena, self, msg); + return @import("domains/network.zig").httpRequestDone(self, msg); } pub fn onHttpResponseData(ctx: *anyopaque, msg: *const Notification.ResponseData) !void { @@ -657,7 +652,7 @@ pub fn BrowserContext(comptime CDP_T: type) type { 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.notification_arena, self, data); + try @import("domains/fetch.zig").requestAuthRequired(self, data); } fn resetNotificationArena(self: *Self) void { diff --git a/src/cdp/domains/accessibility.zig b/src/cdp/domains/accessibility.zig index cfb670fe..f8e8df30 100644 --- a/src/cdp/domains/accessibility.zig +++ b/src/cdp/domains/accessibility.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const std = @import("std"); +const id = @import("../id.zig"); pub fn processMessage(cmd: anytype) !void { const action = std.meta.stringToEnum(enum { @@ -46,15 +47,18 @@ fn getFullAXTree(cmd: anytype) !void { })) orelse return error.InvalidParams; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; + const session = bc.session; - if (params.frameId) |frameId| { - const target_id = bc.target_id orelse return error.TargetNotLoaded; - if (std.mem.eql(u8, target_id, frameId) == false) { + const page = blk: { + const frame_id = params.frameId orelse { + break :blk session.currentPage() orelse return error.PageNotLoaded; + }; + const page_id = try id.toPageId(.frame_id, frame_id); + break :blk session.findPage(page_id) orelse { return cmd.sendError(-32000, "Frame with the given id does not belong to the target.", .{}); - } - } + }; + }; - const page = bc.session.currentPage() orelse return error.PageNotLoaded; const doc = page.window._document.asNode(); const node = try bc.node_registry.register(doc); diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index d6900ced..7bd89827 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const std = @import("std"); +const id = @import("../id.zig"); const log = @import("../../log.zig"); const Node = @import("../Node.zig"); const DOMNode = @import("../../browser/webapi/Node.zig"); @@ -499,12 +500,11 @@ fn getFrameOwner(cmd: anytype) !void { })) orelse return error.InvalidParams; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - const target_id = bc.target_id orelse return error.TargetNotLoaded; - if (std.mem.eql(u8, target_id, params.frameId) == false) { - return cmd.sendError(-32000, "Frame with the given id does not belong to the target.", .{}); - } + const page_id = try id.toPageId(.frame_id, params.frameId); - const page = bc.session.currentPage() orelse return error.PageNotLoaded; + const page = bc.session.findPage(page_id) orelse { + return cmd.sendError(-32000, "Frame with the given id does not belong to the target.", .{}); + }; const node = try bc.node_registry.register(page.window._document.asNode()); return cmd.sendResult(.{ .nodeId = node.id, .backendNodeId = node.id }, .{}); diff --git a/src/cdp/domains/fetch.zig b/src/cdp/domains/fetch.zig index d98e204a..d6e96f2f 100644 --- a/src/cdp/domains/fetch.zig +++ b/src/cdp/domains/fetch.zig @@ -19,6 +19,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; +const id = @import("../id.zig"); const log = @import("../../log.zig"); const network = @import("network.zig"); @@ -48,7 +49,7 @@ pub fn processMessage(cmd: anytype) !void { // Stored in CDP pub const InterceptState = struct { allocator: Allocator, - waiting: std.AutoArrayHashMapUnmanaged(u64, *Http.Transfer), + waiting: std.AutoArrayHashMapUnmanaged(u32, *Http.Transfer), pub fn init(allocator: Allocator) !InterceptState { return .{ @@ -65,8 +66,8 @@ pub const InterceptState = struct { return self.waiting.put(self.allocator, transfer.id, transfer); } - pub fn remove(self: *InterceptState, id: u64) ?*Http.Transfer { - const entry = self.waiting.fetchSwapRemove(id) orelse return null; + pub fn remove(self: *InterceptState, request_id: u32) ?*Http.Transfer { + const entry = self.waiting.fetchSwapRemove(request_id) orelse return null; return entry.value; } @@ -178,13 +179,11 @@ fn arePatternsSupported(patterns: []RequestPattern) bool { return true; } -pub fn requestIntercept(arena: Allocator, bc: anytype, intercept: *const Notification.RequestIntercept) !void { +pub fn requestIntercept(bc: anytype, 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; - const target_id = bc.target_id orelse unreachable; - // We keep it around to wait for modifications to the request. // NOTE: we assume whomever created the request created it with a lifetime of the Page. // TODO: What to do when receiving replies for a previous page's requests? @@ -193,16 +192,16 @@ pub fn requestIntercept(arena: Allocator, bc: anytype, intercept: *const Notific try bc.intercept_state.put(transfer); try bc.cdp.sendEvent("Fetch.requestPaused", .{ - .requestId = try std.fmt.allocPrint(arena, "INTERCEPT-{d}", .{transfer.id}), + .requestId = &id.toInterceptId(transfer.id), + .frameId = &id.toFrameId(transfer.req.page_id), .request = network.TransferAsRequestWriter.init(transfer), - .frameId = target_id, .resourceType = switch (transfer.req.resource_type) { .script => "Script", .xhr => "XHR", .document => "Document", .fetch => "Fetch", }, - .networkId = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id}), + .networkId = &id.toRequestId(transfer.id), // matches the Network REQ-ID }, .{ .session_id = session_id }); log.debug(.cdp, "request intercept", .{ @@ -218,7 +217,7 @@ pub fn requestIntercept(arena: Allocator, bc: anytype, intercept: *const Notific fn continueRequest(cmd: anytype) !void { const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const params = (try cmd.params(struct { - requestId: []const u8, // "INTERCEPT-{d}" + requestId: []const u8, // INT-{d}" url: ?[]const u8 = null, method: ?[]const u8 = null, postData: ?[]const u8 = null, @@ -278,7 +277,7 @@ const AuthChallengeResponse = enum { fn continueWithAuth(cmd: anytype) !void { const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const params = (try cmd.params(struct { - requestId: []const u8, // "INTERCEPT-{d}" + requestId: []const u8, // "INT-{d}" authChallengeResponse: struct { response: AuthChallengeResponse, username: []const u8 = "", @@ -322,7 +321,7 @@ fn fulfillRequest(cmd: anytype) !void { const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const params = (try cmd.params(struct { - requestId: []const u8, // "INTERCEPT-{d}" + requestId: []const u8, // "INT-{d}" responseCode: u16, responseHeaders: ?[]const Http.Header = null, binaryResponseHeaders: ?[]const u8 = null, @@ -363,7 +362,7 @@ fn fulfillRequest(cmd: anytype) !void { fn failRequest(cmd: anytype) !void { const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const params = (try cmd.params(struct { - requestId: []const u8, // "INTERCEPT-{d}" + requestId: []const u8, // "INT-{d}" errorReason: ErrorReason, })) orelse return error.InvalidParams; @@ -382,13 +381,11 @@ fn failRequest(cmd: anytype) !void { return cmd.sendResult(null, .{}); } -pub fn requestAuthRequired(arena: Allocator, bc: anytype, intercept: *const Notification.RequestAuthRequired) !void { +pub fn requestAuthRequired(bc: anytype, 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; - const target_id = bc.target_id orelse unreachable; - // We keep it around to wait for modifications to the request. // NOTE: we assume whomever created the request created it with a lifetime of the Page. // TODO: What to do when receiving replies for a previous page's requests? @@ -399,9 +396,9 @@ pub fn requestAuthRequired(arena: Allocator, bc: anytype, intercept: *const Noti const challenge = transfer._auth_challenge orelse return error.NullAuthChallenge; try bc.cdp.sendEvent("Fetch.authRequired", .{ - .requestId = try std.fmt.allocPrint(arena, "INTERCEPT-{d}", .{transfer.id}), + .requestId = &id.toInterceptId(transfer.id), + .frameId = &id.toFrameId(transfer.req.page_id), .request = network.TransferAsRequestWriter.init(transfer), - .frameId = target_id, .resourceType = switch (transfer.req.resource_type) { .script => "Script", .xhr => "XHR", @@ -414,7 +411,7 @@ pub fn requestAuthRequired(arena: Allocator, bc: anytype, intercept: *const Noti .scheme = if (challenge.scheme == .digest) "digest" else "basic", .realm = challenge.realm, }, - .networkId = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id}), + .networkId = &id.toRequestId(transfer.id), }, .{ .session_id = session_id }); log.debug(.cdp, "request auth required", .{ @@ -427,10 +424,10 @@ pub fn requestAuthRequired(arena: Allocator, bc: anytype, intercept: *const Noti intercept.wait_for_interception.* = true; } -// Get u64 from requestId which is formatted as: "INTERCEPT-{d}" -fn idFromRequestId(request_id: []const u8) !u64 { - if (!std.mem.startsWith(u8, request_id, "INTERCEPT-")) { +// Get u32 from requestId which is formatted as: "INT-{d}" +fn idFromRequestId(request_id: []const u8) !u32 { + if (!std.mem.startsWith(u8, request_id, "INT-")) { return error.InvalidParams; } - return std.fmt.parseInt(u64, request_id[10..], 10) catch return error.InvalidParams; + return std.fmt.parseInt(u32, request_id[4..], 10) catch return error.InvalidParams; } diff --git a/src/cdp/domains/input.zig b/src/cdp/domains/input.zig index 464a4feb..965b395a 100644 --- a/src/cdp/domains/input.zig +++ b/src/cdp/domains/input.zig @@ -116,30 +116,3 @@ fn insertText(cmd: anytype) !void { try cmd.sendResult(null, .{}); } - -fn clickNavigate(cmd: anytype, uri: std.Uri) !void { - const bc = cmd.browser_context.?; - - var url_buf: std.ArrayList(u8) = .{}; - try uri.writeToStream(.{ - .scheme = true, - .authentication = true, - .authority = true, - .port = true, - .path = true, - .query = true, - }, url_buf.writer(cmd.arena)); - const url = url_buf.items; - - try cmd.sendEvent("Page.frameRequestedNavigation", .{ - .url = url, - .frameId = bc.target_id.?, - .reason = "anchorClick", - .disposition = "currentTab", - }, .{ .session_id = bc.session_id.? }); - - try bc.session.removePage(); - _ = try bc.session.createPage(null); - - try @import("page.zig").navigateToUrl(cmd, url, false); -} diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig index a2fac2da..bd175978 100644 --- a/src/cdp/domains/network.zig +++ b/src/cdp/domains/network.zig @@ -21,6 +21,8 @@ const lp = @import("lightpanda"); const Allocator = std.mem.Allocator; const CdpStorage = @import("storage.zig"); + +const id = @import("../id.zig"); const URL = @import("../../browser/URL.zig"); const Transfer = @import("../../http/Client.zig").Transfer; const Notification = @import("../../Notification.zig"); @@ -208,7 +210,7 @@ fn getResponseBody(cmd: anytype) !void { }, .{}); } -pub fn httpRequestFail(arena: Allocator, bc: anytype, msg: *const Notification.RequestFail) !void { +pub fn httpRequestFail(bc: anytype, 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. @@ -220,7 +222,7 @@ pub fn httpRequestFail(arena: Allocator, bc: anytype, msg: *const Notification.R // We're missing a bunch of fields, but, for now, this seems like enough try bc.cdp.sendEvent("Network.loadingFailed", .{ - .requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{msg.transfer.id}), + .requestId = &id.toRequestId(msg.transfer.id), // Seems to be what chrome answers with. I assume it depends on the type of error? .type = "Ping", .errorText = msg.err, @@ -228,28 +230,27 @@ pub fn httpRequestFail(arena: Allocator, bc: anytype, msg: *const Notification.R }, .{ .session_id = session_id }); } -pub fn httpRequestStart(arena: Allocator, bc: anytype, msg: *const Notification.RequestStart) !void { +pub fn httpRequestStart(bc: anytype, 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; - const target_id = bc.target_id orelse unreachable; - const page = bc.session.currentPage() orelse unreachable; + const transfer = msg.transfer; + const req = &transfer.req; + const page_id = req.page_id; + const page = bc.session.findPage(page_id) orelse return; // Modify request with extra CDP headers for (bc.extra_headers.items) |extra| { - try msg.transfer.req.headers.add(extra); + try req.headers.add(extra); } - const transfer = msg.transfer; - const loader_id = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id}); - // We're missing a bunch of fields, but, for now, this seems like enough try bc.cdp.sendEvent("Network.requestWillBeSent", .{ - .requestId = loader_id, - .frameId = target_id, - .loaderId = loader_id, - .type = msg.transfer.req.resource_type.string(), + .loaderId = &id.toLoaderId(transfer.id), + .requestId = &id.toRequestId(transfer.id), + .frameId = &id.toFrameId(page_id), + .type = req.resource_type.string(), .documentURL = page.url, .request = TransferAsRequestWriter.init(transfer), .initiator = .{ .type = "other" }, @@ -262,29 +263,27 @@ pub fn httpResponseHeaderDone(arena: Allocator, bc: anytype, msg: *const Notific // 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; - const target_id = bc.target_id orelse unreachable; const transfer = msg.transfer; - const loader_id = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id}); // We're missing a bunch of fields, but, for now, this seems like enough try bc.cdp.sendEvent("Network.responseReceived", .{ - .requestId = loader_id, - .frameId = target_id, - .loaderId = loader_id, + .loaderId = &id.toLoaderId(transfer.id), + .requestId = &id.toRequestId(transfer.id), + .frameId = &id.toFrameId(transfer.req.page_id), .response = TransferAsResponseWriter.init(arena, msg.transfer), .hasExtraInfo = false, // TODO change after adding Network.responseReceivedExtraInfo }, .{ .session_id = session_id }); } -pub fn httpRequestDone(arena: Allocator, bc: anytype, msg: *const Notification.RequestDone) !void { +pub fn httpRequestDone(bc: anytype, 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; - + const transfer = msg.transfer; try bc.cdp.sendEvent("Network.loadingFinished", .{ - .requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{msg.transfer.id}), - .encodedDataLength = msg.transfer.bytes_received, + .requestId = &id.toRequestId(transfer.id), + .encodedDataLength = transfer.bytes_received, }, .{ .session_id = session_id }); } diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index eee83f77..dad180e5 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -19,6 +19,7 @@ const std = @import("std"); const lp = @import("lightpanda"); +const id = @import("../id.zig"); const log = @import("../../log.zig"); const js = @import("../../browser/js/js.zig"); const Page = @import("../../browser/Page.zig"); @@ -73,9 +74,9 @@ fn getFrameTree(cmd: anytype) !void { return cmd.sendResult(.{ .frameTree = .{ .frame = Frame{ - .id = target_id, - .loaderId = bc.loader_id, + .id = &target_id, .securityOrigin = bc.security_origin, + .loaderId = "LID-0000000001", .url = bc.getURL() orelse "about:blank", .secureContextType = bc.secure_context_type, }, @@ -103,18 +104,21 @@ fn setLifecycleEventsEnabled(cmd: anytype) !void { const page = bc.session.currentPage() orelse return error.PageNotLoaded; if (page._load_state == .complete) { + const frame_id = &id.toFrameId(page.id); + const loader_id = &id.toLoaderId(page._req_id); + const now = timestampF(.monotonic); - try sendPageLifecycle(bc, "DOMContentLoaded", now); - try sendPageLifecycle(bc, "load", now); + try sendPageLifecycle(bc, "DOMContentLoaded", now, frame_id, loader_id); + try sendPageLifecycle(bc, "load", now, frame_id, loader_id); const http_client = page._session.browser.http_client; const http_active = http_client.active; const total_network_activity = http_active + http_client.intercepted; if (page._notified_network_almost_idle.check(total_network_activity <= 2)) { - try sendPageLifecycle(bc, "networkAlmostIdle", now); + try sendPageLifecycle(bc, "networkAlmostIdle", now, frame_id, loader_id); } if (page._notified_network_idle.check(total_network_activity == 0)) { - try sendPageLifecycle(bc, "networkIdle", now); + try sendPageLifecycle(bc, "networkIdle", now, frame_id, loader_id); } } @@ -227,16 +231,15 @@ fn navigate(cmd: anytype) !void { }); } -pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.PageNavigate) !void { +pub fn pageNavigate(bc: anytype, 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; - - const loader_id = try std.fmt.allocPrint(arena, "REQ-{d}", .{event.req_id}); - const target_id = bc.target_id orelse unreachable; - bc.reset(); + const frame_id = &id.toFrameId(event.page_id); + const loader_id = &id.toLoaderId(event.req_id); + var cdp = bc.cdp; const reason_: ?[]const u8 = switch (event.opts.reason) { .anchor => "anchorClick", @@ -250,14 +253,14 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa }; if (reason_) |reason| { try cdp.sendEvent("Page.frameScheduledNavigation", .{ - .frameId = target_id, + .frameId = frame_id, .delay = 0, .reason = reason, .url = event.url, }, .{ .session_id = session_id }); try cdp.sendEvent("Page.frameRequestedNavigation", .{ - .frameId = target_id, + .frameId = frame_id, .reason = reason, .url = event.url, .disposition = "currentTab", @@ -266,7 +269,7 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa // frameStartedNavigating event try cdp.sendEvent("Page.frameStartedNavigating", .{ - .frameId = target_id, + .frameId = frame_id, .url = event.url, .loaderId = loader_id, .navigationType = "differentDocument", @@ -274,7 +277,7 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa // frameStartedLoading event try cdp.sendEvent("Page.frameStartedLoading", .{ - .frameId = target_id, + .frameId = frame_id, }, .{ .session_id = session_id }); } @@ -301,9 +304,10 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P // 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; - const loader_id = try std.fmt.allocPrint(arena, "REQ-{d}", .{event.req_id}); - const target_id = bc.target_id orelse unreachable; + const timestamp = event.timestamp; + const frame_id = &id.toFrameId(event.page_id); + const loader_id = &id.toLoaderId(event.req_id); var cdp = bc.cdp; @@ -316,7 +320,7 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P try cdp.sendJSON(.{ .id = input_id, .result = .{ - .frameId = target_id, + .frameId = frame_id, .loaderId = loader_id, }, .sessionId = session_id, @@ -326,7 +330,7 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P if (bc.page_life_cycle_events) { try cdp.sendEvent("Page.lifecycleEvent", LifecycleEvent{ .name = "init", - .frameId = target_id, + .frameId = frame_id, .loaderId = loader_id, .timestamp = event.timestamp, }, .{ .session_id = session_id }); @@ -345,7 +349,7 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P if (reason_ != null) { try cdp.sendEvent("Page.frameClearedScheduledNavigation", .{ - .frameId = target_id, + .frameId = frame_id, }, .{ .session_id = session_id }); } @@ -356,7 +360,7 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P { const page = bc.session.currentPage() orelse return error.PageNotLoaded; - const aux_data = try std.fmt.allocPrint(arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{target_id}); + const aux_data = try std.fmt.allocPrint(arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\",\"loaderId\":\"{s}\"}}", .{ frame_id, loader_id }); var ls: js.Local.Scope = undefined; page.js.localScope(&ls); @@ -371,7 +375,7 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P ); } for (bc.isolated_worlds.items) |isolated_world| { - const aux_json = try std.fmt.allocPrint(arena, "{{\"isDefault\":false,\"type\":\"isolated\",\"frameId\":\"{s}\"}}", .{target_id}); + const aux_json = try std.fmt.allocPrint(arena, "{{\"isDefault\":false,\"type\":\"isolated\",\"frameId\":\"{s}\",\"loaderId\":\"{s}\"}}", .{ frame_id, loader_id }); // Calling contextCreated will assign a new Id to the context and send the contextCreated event @@ -392,7 +396,7 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P try cdp.sendEvent("Page.frameNavigated", .{ .type = "Navigation", .frame = Frame{ - .id = target_id, + .id = frame_id, .url = event.url, .loaderId = loader_id, .securityOrigin = bc.security_origin, @@ -419,7 +423,7 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P try cdp.sendEvent("Page.lifecycleEvent", LifecycleEvent{ .timestamp = timestamp, .name = "DOMContentLoaded", - .frameId = target_id, + .frameId = frame_id, .loaderId = loader_id, }, .{ .session_id = session_id }); } @@ -436,35 +440,33 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P try cdp.sendEvent("Page.lifecycleEvent", LifecycleEvent{ .timestamp = timestamp, .name = "load", - .frameId = target_id, + .frameId = frame_id, .loaderId = loader_id, }, .{ .session_id = session_id }); } // frameStoppedLoading return cdp.sendEvent("Page.frameStoppedLoading", .{ - .frameId = target_id, + .frameId = frame_id, }, .{ .session_id = session_id }); } pub fn pageNetworkIdle(bc: anytype, event: *const Notification.PageNetworkIdle) !void { - return sendPageLifecycle(bc, "networkIdle", event.timestamp); + return sendPageLifecycle(bc, "networkIdle", event.timestamp, &id.toFrameId(event.page_id), &id.toLoaderId(event.req_id)); } pub fn pageNetworkAlmostIdle(bc: anytype, event: *const Notification.PageNetworkAlmostIdle) !void { - return sendPageLifecycle(bc, "networkAlmostIdle", event.timestamp); + return sendPageLifecycle(bc, "networkAlmostIdle", event.timestamp, &id.toFrameId(event.page_id), &id.toLoaderId(event.req_id)); } -fn sendPageLifecycle(bc: anytype, name: []const u8, timestamp: u64) !void { +fn sendPageLifecycle(bc: anytype, 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; - const loader_id = bc.loader_id; - const target_id = bc.target_id orelse unreachable; return bc.cdp.sendEvent("Page.lifecycleEvent", LifecycleEvent{ .name = name, - .frameId = target_id, + .frameId = frame_id, .loaderId = loader_id, .timestamp = timestamp, }, .{ .session_id = session_id }); @@ -487,15 +489,15 @@ test "cdp.page: getFrameTree" { try ctx.expectSentError(-31998, "BrowserContextNotLoaded", .{ .id = 10 }); } - const bc = try ctx.loadBrowserContext(.{ .id = "BID-9", .target_id = "TID-3" }); + const bc = try ctx.loadBrowserContext(.{ .id = "BID-9", .url = "hi.html", .target_id = "FID-000000000X".* }); { try ctx.processMessage(.{ .id = 11, .method = "Page.getFrameTree" }); try ctx.expectSentResult(.{ .frameTree = .{ .frame = .{ - .id = "TID-3", - .loaderId = bc.loader_id, - .url = "about:blank", + .id = "FID-000000000X", + .loaderId = "LID-0000000001", + .url = "http://127.0.0.1:9582/src/browser/tests/hi.html", .domainAndRegistry = "", .securityOrigin = bc.security_origin, .mimeType = "text/html", diff --git a/src/cdp/domains/target.zig b/src/cdp/domains/target.zig index 32b29901..7dfe59ef 100644 --- a/src/cdp/domains/target.zig +++ b/src/cdp/domains/target.zig @@ -18,6 +18,8 @@ const std = @import("std"); const lp = @import("lightpanda"); + +const id = @import("../id.zig"); const log = @import("../../log.zig"); const js = @import("../../browser/js/js.zig"); @@ -66,11 +68,11 @@ fn getTargets(cmd: anytype) !void { }, .{ .include_session_id = false }); }; - const target_id = bc.target_id orelse { + const target_id = &(bc.target_id orelse { return cmd.sendResult(.{ .targetInfos = [_]TargetInfo{}, }, .{ .include_session_id = false }); - }; + }); return cmd.sendResult(.{ .targetInfos = [_]TargetInfo{.{ @@ -171,11 +173,12 @@ fn createTarget(cmd: anytype) !void { // if target_id is null, we should never have a session_id lp.assert(bc.session_id == null, "CDP.target.createTarget not null session_id", .{}); - const target_id = cmd.cdp.target_id_gen.next(); - - bc.target_id = target_id; - const page = try bc.session.createPage(); + + // the target_id == the frame_id of the "root" page + const frame_id = id.toFrameId(page.id); + bc.target_id = frame_id; + const target_id = &bc.target_id.?; { var ls: js.Local.Scope = undefined; page.js.localScope(&ls); @@ -195,7 +198,6 @@ fn createTarget(cmd: anytype) !void { // change CDP state bc.security_origin = "://"; bc.secure_context_type = "InsecureScheme"; - bc.loader_id = LOADER_ID; // send targetCreated event // TODO: should this only be sent when Target.setDiscoverTargets @@ -234,7 +236,7 @@ fn attachToTarget(cmd: anytype) !void { })) orelse return error.InvalidParams; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - const target_id = bc.target_id orelse return error.TargetNotLoaded; + const target_id = &(bc.target_id orelse return error.TargetNotLoaded); if (std.mem.eql(u8, target_id, params.targetId) == false) { return error.UnknownTargetId; } @@ -255,7 +257,7 @@ fn closeTarget(cmd: anytype) !void { })) orelse return error.InvalidParams; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - const target_id = bc.target_id orelse return error.TargetNotLoaded; + const target_id = &(bc.target_id orelse return error.TargetNotLoaded); if (std.mem.eql(u8, target_id, params.targetId) == false) { return error.UnknownTargetId; } @@ -298,7 +300,7 @@ fn getTargetInfo(cmd: anytype) !void { if (params.targetId) |param_target_id| { const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - const target_id = bc.target_id orelse return error.TargetNotLoaded; + const target_id = &(bc.target_id orelse return error.TargetNotLoaded); if (std.mem.eql(u8, target_id, param_target_id) == false) { return error.UnknownTargetId; } @@ -415,10 +417,11 @@ fn setAutoAttach(cmd: anytype) !void { // autoAttach is set to true, we must attach to all existing targets. if (cmd.browser_context) |bc| { if (bc.target_id == null) { - // hasn't attached yet - const target_id = cmd.cdp.target_id_gen.next(); - try doAttachtoTarget(cmd, target_id); - bc.target_id = target_id; + if (bc.session.currentPage()) |page| { + // the target_id == the frame_id of the "root" page + bc.target_id = id.toFrameId(page.id); + try doAttachtoTarget(cmd, &bc.target_id.?); + } } // should we send something here? return; @@ -612,14 +615,14 @@ test "cdp.target: closeTarget" { // pretend we createdTarget first _ = try bc.session.createPage(); - bc.target_id = "TID-A"; + bc.target_id = "TID-000000000A".*; { try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "TID-8" } })); try ctx.expectSentError(-31998, "UnknownTargetId", .{ .id = 10 }); } { - try ctx.processMessage(.{ .id = 11, .method = "Target.closeTarget", .params = .{ .targetId = "TID-A" } }); + try ctx.processMessage(.{ .id = 11, .method = "Target.closeTarget", .params = .{ .targetId = "TID-000000000A" } }); try ctx.expectSentResult(.{ .success = true }, .{ .id = 11 }); try testing.expectEqual(null, bc.session.page); try testing.expectEqual(null, bc.target_id); @@ -643,14 +646,14 @@ test "cdp.target: attachToTarget" { // pretend we createdTarget first _ = try bc.session.createPage(); - bc.target_id = "TID-B"; + bc.target_id = "TID-000000000B".*; { try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-8" } })); try ctx.expectSentError(-31998, "UnknownTargetId", .{ .id = 10 }); } { - try ctx.processMessage(.{ .id = 11, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-B" } }); + try ctx.processMessage(.{ .id = 11, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-000000000B" } }); const session_id = bc.session_id.?; try ctx.expectSentResult(.{ .sessionId = session_id }, .{ .id = 11 }); try ctx.expectSentEvent("Target.attachedToTarget", .{ .sessionId = session_id, .targetInfo = .{ .url = "chrome://newtab/", .title = "about:blank", .attached = true, .type = "page", .canAccessOpener = false, .browserContextId = "BID-9", .targetId = bc.target_id.? } }, .{}); @@ -687,17 +690,17 @@ test "cdp.target: getTargetInfo" { // pretend we createdTarget first _ = try bc.session.createPage(); - bc.target_id = "TID-A"; + bc.target_id = "TID-000000000C".*; { try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.getTargetInfo", .params = .{ .targetId = "TID-8" } })); try ctx.expectSentError(-31998, "UnknownTargetId", .{ .id = 10 }); } { - try ctx.processMessage(.{ .id = 11, .method = "Target.getTargetInfo", .params = .{ .targetId = "TID-A" } }); + try ctx.processMessage(.{ .id = 11, .method = "Target.getTargetInfo", .params = .{ .targetId = "TID-000000000C" } }); try ctx.expectSentResult(.{ .targetInfo = .{ - .targetId = "TID-A", + .targetId = "TID-000000000C", .type = "page", .title = "", .url = "about:blank", diff --git a/src/cdp/id.zig b/src/cdp/id.zig new file mode 100644 index 00000000..df580e8c --- /dev/null +++ b/src/cdp/id.zig @@ -0,0 +1,184 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// 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 . + +const std = @import("std"); +const IS_DEBUG = @import("builtin").mode == .Debug; + +pub fn toPageId(comptime id_type: enum { frame_id, loader_id }, input: []const u8) !u32 { + const err = switch (comptime id_type) { + .frame_id => error.InvalidFrameId, + .loader_id => error.InvalidLoaderId, + }; + + if (input.len < 4) { + return err; + } + + return std.fmt.parseInt(u32, input[4..], 10) catch err; +} + +pub fn toFrameId(page_id: u32) [14]u8 { + var buf: [14]u8 = undefined; + _ = std.fmt.bufPrint(&buf, "FID-{d:0>10}", .{page_id}) catch unreachable; + return buf; +} + +pub fn toLoaderId(page_id: u32) [14]u8 { + var buf: [14]u8 = undefined; + _ = std.fmt.bufPrint(&buf, "LID-{d:0>10}", .{page_id}) catch unreachable; + return buf; +} + +pub fn toRequestId(page_id: u32) [14]u8 { + var buf: [14]u8 = undefined; + _ = std.fmt.bufPrint(&buf, "RID-{d:0>10}", .{page_id}) catch unreachable; + return buf; +} + +pub fn toInterceptId(page_id: u32) [14]u8 { + var buf: [14]u8 = undefined; + _ = std.fmt.bufPrint(&buf, "INT-{d:0>10}", .{page_id}) catch unreachable; + return buf; +} + +// Generates incrementing prefixed integers, i.e. CTX-1, CTX-2, CTX-3. +// Wraps to 0 on overflow. +// Many caveats for using this: +// - Not thread-safe. +// - Information leaking +// - The slice returned by next() is only valid: +// - while incrementor is valid +// - until the next call to next() +// On the positive, it's zero allocation +pub fn Incrementing(comptime T: type, comptime prefix: []const u8) type { + // +1 for the '-' separator + const NUMERIC_START = prefix.len + 1; + const MAX_BYTES = NUMERIC_START + switch (T) { + u8 => 3, + u16 => 5, + u32 => 10, + u64 => 20, + else => @compileError("Incrementing must be given an unsigned int type, got: " ++ @typeName(T)), + }; + + const buffer = blk: { + var b = [_]u8{0} ** MAX_BYTES; + @memcpy(b[0..prefix.len], prefix); + b[prefix.len] = '-'; + break :blk b; + }; + + const PrefixIntType = @Type(.{ .int = .{ + .bits = NUMERIC_START * 8, + .signedness = .unsigned, + } }); + + const PREFIX_INT_CODE: PrefixIntType = @bitCast(buffer[0..NUMERIC_START].*); + + return struct { + counter: T = 0, + buffer: [MAX_BYTES]u8 = buffer, + + const Self = @This(); + + pub fn next(self: *Self) []const u8 { + const counter = self.counter; + const n = counter +% 1; + defer self.counter = n; + + const size = std.fmt.printInt(self.buffer[NUMERIC_START..], n, 10, .lower, .{}); + return self.buffer[0 .. NUMERIC_START + size]; + } + + // extracts the numeric portion from an ID + pub fn parse(str: []const u8) !T { + if (str.len <= NUMERIC_START) { + return error.InvalidId; + } + + if (@as(PrefixIntType, @bitCast(str[0..NUMERIC_START].*)) != PREFIX_INT_CODE) { + return error.InvalidId; + } + + return std.fmt.parseInt(T, str[NUMERIC_START..], 10) catch { + return error.InvalidId; + }; + } + }; +} + +const testing = @import("../testing.zig"); +test "id: Incrementing.next" { + var id = Incrementing(u16, "IDX"){}; + try testing.expectEqual("IDX-1", id.next()); + try testing.expectEqual("IDX-2", id.next()); + try testing.expectEqual("IDX-3", id.next()); + + // force a wrap + id.counter = 65533; + try testing.expectEqual("IDX-65534", id.next()); + try testing.expectEqual("IDX-65535", id.next()); + try testing.expectEqual("IDX-0", id.next()); +} + +test "id: Incrementing.parse" { + const ReqId = Incrementing(u32, "REQ"); + try testing.expectError(error.InvalidId, ReqId.parse("")); + try testing.expectError(error.InvalidId, ReqId.parse("R")); + try testing.expectError(error.InvalidId, ReqId.parse("RE")); + try testing.expectError(error.InvalidId, ReqId.parse("REQ")); + try testing.expectError(error.InvalidId, ReqId.parse("REQ-")); + try testing.expectError(error.InvalidId, ReqId.parse("REQ--1")); + try testing.expectError(error.InvalidId, ReqId.parse("REQ--")); + try testing.expectError(error.InvalidId, ReqId.parse("REQ-Nope")); + try testing.expectError(error.InvalidId, ReqId.parse("REQ-4294967296")); + + try testing.expectEqual(0, try ReqId.parse("REQ-0")); + try testing.expectEqual(99, try ReqId.parse("REQ-99")); + try testing.expectEqual(4294967295, try ReqId.parse("REQ-4294967295")); +} + +test "id: toPageId" { + try testing.expectEqual(0, toPageId(.frame_id, "FID-0")); + try testing.expectEqual(0, toPageId(.loader_id, "LID-0")); + + try testing.expectEqual(4294967295, toPageId(.frame_id, "FID-4294967295")); + try testing.expectEqual(4294967295, toPageId(.loader_id, "LID-4294967295")); + try testing.expectError(error.InvalidFrameId, toPageId(.frame_id, "")); + try testing.expectError(error.InvalidLoaderId, toPageId(.loader_id, "LID-NOPE")); +} + +test "id: toFrameId" { + try testing.expectEqual("FID-0000000000", toFrameId(0)); + try testing.expectEqual("FID-4294967295", toFrameId(4294967295)); +} + +test "id: toLoaderId" { + try testing.expectEqual("LID-0000000000", toLoaderId(0)); + try testing.expectEqual("LID-4294967295", toLoaderId(4294967295)); +} + +test "id: toRequestId" { + try testing.expectEqual("RID-0000000000", toRequestId(0)); + try testing.expectEqual("RID-4294967295", toRequestId(4294967295)); +} + +test "id: toInterceptId" { + try testing.expectEqual("INT-0000000000", toInterceptId(0)); + try testing.expectEqual("INT-4294967295", toInterceptId(4294967295)); +} diff --git a/src/cdp/testing.zig b/src/cdp/testing.zig index 6a98a71e..1093afdc 100644 --- a/src/cdp/testing.zig +++ b/src/cdp/testing.zig @@ -92,7 +92,7 @@ const TestContext = struct { const BrowserContextOpts = struct { id: ?[]const u8 = null, - target_id: ?[]const u8 = null, + target_id: ?[14]u8 = null, session_id: ?[]const u8 = null, url: ?[:0]const u8 = null, }; @@ -122,7 +122,7 @@ const TestContext = struct { bc.session_id = "SID-X"; } if (bc.target_id == null) { - bc.target_id = "TID-X"; + bc.target_id = "TID-000000000Z".*; } const page = try bc.session.createPage(); const full_url = try std.fmt.allocPrintSentinel( diff --git a/src/http/Client.zig b/src/http/Client.zig index 1a38ef9e..65c859a8 100644 --- a/src/http/Client.zig +++ b/src/http/Client.zig @@ -79,7 +79,7 @@ multi: *c.CURLM, handles: Handles, // Use to generate the next request ID -next_request_id: u64 = 0, +next_request_id: u32 = 0, // When handles has no more available easys, requests get queued. queue: TransferQueue, @@ -336,6 +336,7 @@ fn fetchRobotsThenProcessRequest(self: *Client, robots_url: [:0]const u8, req: R .method = .GET, .headers = headers, .blocking = false, + .page_id = req.page_id, .cookie_jar = req.cookie_jar, .notification = req.notification, .resource_type = .fetch, @@ -562,12 +563,12 @@ pub fn fulfillTransfer(self: *Client, transfer: *Transfer, status: u16, headers: transfer._intercept_state = .fulfilled; } -pub fn nextReqId(self: *Client) usize { - return self.next_request_id + 1; +pub fn nextReqId(self: *Client) u32 { + return self.next_request_id +% 1; } -pub fn incrReqId(self: *Client) usize { - const id = self.next_request_id + 1; +pub fn incrReqId(self: *Client) u32 { + const id = self.next_request_id +% 1; self.next_request_id = id; return id; } @@ -1003,6 +1004,7 @@ pub const RequestCookie = struct { }; pub const Request = struct { + page_id: u32, method: Method, url: [:0]const u8, headers: Http.Headers, @@ -1093,7 +1095,7 @@ pub const AuthChallenge = struct { pub const Transfer = struct { arena: ArenaAllocator, - id: usize = 0, + id: u32 = 0, req: Request, url: [:0]const u8, ctx: *anyopaque, // copied from req.ctx to make it easier for callback handlers diff --git a/src/id.zig b/src/id.zig index b45df0ce..f2a9abf8 100644 --- a/src/id.zig +++ b/src/id.zig @@ -19,72 +19,6 @@ const std = @import("std"); const lp = @import("lightpanda"); -// Generates incrementing prefixed integers, i.e. CTX-1, CTX-2, CTX-3. -// Wraps to 0 on overflow. -// Many caveats for using this: -// - Not thread-safe. -// - Information leaking -// - The slice returned by next() is only valid: -// - while incrementor is valid -// - until the next call to next() -// On the positive, it's zero allocation -pub fn Incrementing(comptime T: type, comptime prefix: []const u8) type { - // +1 for the '-' separator - const NUMERIC_START = prefix.len + 1; - const MAX_BYTES = NUMERIC_START + switch (T) { - u8 => 3, - u16 => 5, - u32 => 10, - u64 => 20, - else => @compileError("Incrementing must be given an unsigned int type, got: " ++ @typeName(T)), - }; - - const buffer = blk: { - var b = [_]u8{0} ** MAX_BYTES; - @memcpy(b[0..prefix.len], prefix); - b[prefix.len] = '-'; - break :blk b; - }; - - const PrefixIntType = @Type(.{ .int = .{ - .bits = NUMERIC_START * 8, - .signedness = .unsigned, - } }); - - const PREFIX_INT_CODE: PrefixIntType = @bitCast(buffer[0..NUMERIC_START].*); - - return struct { - counter: T = 0, - buffer: [MAX_BYTES]u8 = buffer, - - const Self = @This(); - - pub fn next(self: *Self) []const u8 { - const counter = self.counter; - const n = counter +% 1; - defer self.counter = n; - - const size = std.fmt.printInt(self.buffer[NUMERIC_START..], n, 10, .lower, .{}); - return self.buffer[0 .. NUMERIC_START + size]; - } - - // extracts the numeric portion from an ID - pub fn parse(str: []const u8) !T { - if (str.len <= NUMERIC_START) { - return error.InvalidId; - } - - if (@as(PrefixIntType, @bitCast(str[0..NUMERIC_START].*)) != PREFIX_INT_CODE) { - return error.InvalidId; - } - - return std.fmt.parseInt(T, str[NUMERIC_START..], 10) catch { - return error.InvalidId; - }; - } - }; -} - pub fn uuidv4(hex: []u8) void { lp.assert(hex.len == 36, "uuidv4.len", .{ .len = hex.len }); @@ -108,36 +42,6 @@ pub fn uuidv4(hex: []u8) void { } const testing = std.testing; -test "id: Incrementing.next" { - var id = Incrementing(u16, "IDX"){}; - try testing.expectEqualStrings("IDX-1", id.next()); - try testing.expectEqualStrings("IDX-2", id.next()); - try testing.expectEqualStrings("IDX-3", id.next()); - - // force a wrap - id.counter = 65533; - try testing.expectEqualStrings("IDX-65534", id.next()); - try testing.expectEqualStrings("IDX-65535", id.next()); - try testing.expectEqualStrings("IDX-0", id.next()); -} - -test "id: Incrementing.parse" { - const ReqId = Incrementing(u32, "REQ"); - try testing.expectError(error.InvalidId, ReqId.parse("")); - try testing.expectError(error.InvalidId, ReqId.parse("R")); - try testing.expectError(error.InvalidId, ReqId.parse("RE")); - try testing.expectError(error.InvalidId, ReqId.parse("REQ")); - try testing.expectError(error.InvalidId, ReqId.parse("REQ-")); - try testing.expectError(error.InvalidId, ReqId.parse("REQ--1")); - try testing.expectError(error.InvalidId, ReqId.parse("REQ--")); - try testing.expectError(error.InvalidId, ReqId.parse("REQ-Nope")); - try testing.expectError(error.InvalidId, ReqId.parse("REQ-4294967296")); - - try testing.expectEqual(0, try ReqId.parse("REQ-0")); - try testing.expectEqual(99, try ReqId.parse("REQ-99")); - try testing.expectEqual(4294967295, try ReqId.parse("REQ-4294967295")); -} - test "id: uuiv4" { const expectUUID = struct { fn expect(uuid: [36]u8) !void {