From bb1ea39c5454b0e7178af2890306d4da3faeb1ad Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 19 Dec 2025 10:31:07 +0800 Subject: [PATCH] backport a variety of smaller CDP changes --- src/browser/Page.zig | 7 +++++ src/cdp/cdp.zig | 8 ++++++ src/cdp/domains/page.zig | 39 +++++++++++++++++++++++++ src/cdp/domains/target.zig | 59 +++++++++++++++++++++++++++++--------- 4 files changed, 99 insertions(+), 14 deletions(-) diff --git a/src/browser/Page.zig b/src/browser/Page.zig index c7b453d8..799052c3 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -283,6 +283,13 @@ fn registerBackgroundTasks(self: *Page) !void { }.runMessageLoop, 250, .{ .name = "page.messageLoop" }); } +pub fn getTitle(self: *Page) !?[]const u8 { + if (self.window._document.is(Document.HTMLDocument)) |html_doc| { + return try html_doc.getTitle(self); + } + return null; +} + pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts, kind: NavigationKind) !void { const session = self._session; diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index a2ce7159..30246788 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -469,6 +469,14 @@ pub fn BrowserContext(comptime CDP_T: type) type { return if (url.len == 0) null else url; } + 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 networkEnable(self: *Self) !void { try self.cdp.browser.notification.register(.http_request_fail, self, onHttpRequestFail); try self.cdp.browser.notification.register(.http_request_start, self, onHttpRequestStart); diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index 9c5e48e4..7f0928eb 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -33,6 +33,7 @@ pub fn processMessage(cmd: anytype) !void { createIsolatedWorld, navigate, stopLoading, + close, }, cmd.input.action) orelse return error.UnknownMethod; switch (action) { @@ -43,6 +44,7 @@ pub fn processMessage(cmd: anytype) !void { .createIsolatedWorld => return createIsolatedWorld(cmd), .navigate => return navigate(cmd), .stopLoading => return cmd.sendResult(null, .{}), + .close => return close(cmd), } } @@ -133,6 +135,43 @@ fn addScriptToEvaluateOnNewDocument(cmd: anytype) !void { }, .{}); } +fn close(cmd: anytype) !void { + const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; + + const target_id = bc.target_id orelse return error.TargetNotLoaded; + + // can't be null if we have a target_id + std.debug.assert(bc.session.page != null); + + try cmd.sendResult(.{}, .{}); + + // Following code is similar to target.closeTarget + // + // could be null, created but never attached + if (bc.session_id) |session_id| { + // Inspector.detached event + try cmd.sendEvent("Inspector.detached", .{ + .reason = "Render process gone.", + }, .{ .session_id = session_id }); + + // detachedFromTarget event + try cmd.sendEvent("Target.detachedFromTarget", .{ + .targetId = target_id, + .sessionId = session_id, + .reason = "Render process gone.", + }, .{}); + + bc.session_id = null; + } + + bc.session.removePage(); + for (bc.isolated_worlds.items) |*world| { + world.deinit(); + } + bc.isolated_worlds.clearRetainingCapacity(); + bc.target_id = null; +} + fn createIsolatedWorld(cmd: anytype) !void { const params = (try cmd.params(struct { frameId: []const u8, diff --git a/src/cdp/domains/target.zig b/src/cdp/domains/target.zig index d6f672e2..7dd636e6 100644 --- a/src/cdp/domains/target.zig +++ b/src/cdp/domains/target.zig @@ -24,6 +24,7 @@ const LOADER_ID = "LOADERID42AA389647D702B4D805F49A"; pub fn processMessage(cmd: anytype) !void { const action = std.meta.stringToEnum(enum { + getTargets, attachToTarget, closeTarget, createBrowserContext, @@ -38,6 +39,7 @@ pub fn processMessage(cmd: anytype) !void { }, cmd.input.action) orelse return error.UnknownMethod; switch (action) { + .getTargets => return getTargets(cmd), .attachToTarget => return attachToTarget(cmd), .closeTarget => return closeTarget(cmd), .createBrowserContext => return createBrowserContext(cmd), @@ -52,6 +54,31 @@ pub fn processMessage(cmd: anytype) !void { } } +fn getTargets(cmd: anytype) !void { + // Some clients like Stagehand expects to have an existing context. + const bc = cmd.browser_context orelse cmd.createBrowserContext() catch |err| switch (err) { + error.AlreadyExists => unreachable, + else => return err, + }; + + const target_id = bc.target_id orelse { + return cmd.sendResult(.{ + .targetInfos = [_]TargetInfo{}, + }, .{ .include_session_id = false }); + }; + + return cmd.sendResult(.{ + .targetInfos = [_]TargetInfo{.{ + .targetId = target_id, + .type = "page", + .title = bc.getTitle() orelse "about:blank", + .url = bc.getURL() orelse "about:blank", + .attached = true, + .canAccessOpener = false, + }}, + }, .{ .include_session_id = false }); +} + fn getBrowserContexts(cmd: anytype) !void { var browser_context_ids: []const []const u8 = undefined; if (cmd.browser_context) |bc| { @@ -168,7 +195,7 @@ fn createTarget(cmd: anytype) !void { .targetInfo = TargetInfo{ .attached = false, .targetId = target_id, - .title = params.url, + .title = "about:blank", .browserContextId = bc.id, .url = "about:blank", }, @@ -179,11 +206,13 @@ fn createTarget(cmd: anytype) !void { try doAttachtoTarget(cmd, target_id); } - try page.navigate( - params.url, - .{ .reason = .address_bar }, - .{ .push = null }, - ); + if (!std.mem.eql(u8, "about:blank", params.url)) { + try page.navigate( + params.url, + .{ .reason = .address_bar }, + .{ .push = null }, + ); + } try cmd.sendResult(.{ .targetId = target_id, @@ -206,7 +235,9 @@ fn attachToTarget(cmd: anytype) !void { return error.SessionAlreadyLoaded; } - try doAttachtoTarget(cmd, target_id); + if (bc.session_id == null) { + try doAttachtoTarget(cmd, target_id); + } return cmd.sendResult( .{ .sessionId = bc.session_id }, @@ -272,8 +303,8 @@ fn getTargetInfo(cmd: anytype) !void { .targetInfo = TargetInfo{ .targetId = target_id, .type = "page", - .title = "", - .url = "", + .title = bc.getTitle() orelse "about:blank", + .url = bc.getURL() orelse "about:blank", .attached = true, .canAccessOpener = false, }, @@ -284,8 +315,8 @@ fn getTargetInfo(cmd: anytype) !void { .targetInfo = TargetInfo{ .targetId = "TID-STARTUP-B", .type = "browser", - .title = "", - .url = "", + .title = "about:blank", + .url = "about:blank", .attached = true, .canAccessOpener = false, }, @@ -631,8 +662,8 @@ test "cdp.target: getTargetInfo" { try ctx.expectSentResult(.{ .targetInfo = .{ .type = "browser", - .title = "", - .url = "", + .title = "about:blank", + .url = "about:blank", .attached = true, .canAccessOpener = false, }, @@ -665,7 +696,7 @@ test "cdp.target: getTargetInfo" { .targetId = "TID-A", .type = "page", .title = "", - .url = "", + .url = "about:blank", .attached = true, .canAccessOpener = false, },