diff --git a/src/cdp/CDP.zig b/src/cdp/CDP.zig index 1b2884b70..2627de29a 100644 --- a/src/cdp/CDP.zig +++ b/src/cdp/CDP.zig @@ -50,6 +50,7 @@ pub const URL_BASE = "chrome://newtab/"; const IS_DEBUG = @import("builtin").mode == .Debug; const SessionIdGen = Incrementing(u32, "SID"); +const BrowserSessionIdGen = Incrementing(u32, "BSID"); const BrowserContextIdGen = Incrementing(u32, "BID"); // webmcp tool invocation pub const InvocationIdGen = Incrementing(u32, "INV"); @@ -72,8 +73,14 @@ link: Network.CdpLink, target_auto_attach: bool = false, session_id_gen: SessionIdGen = .{}, +browser_session_id_gen: BrowserSessionIdGen = .{}, browser_context_id_gen: BrowserContextIdGen = .{}, +// Session from Target.attachToBrowserTarget. Distinct from the page +// session (BrowserContext.session_id) as it targets the browser itself and +// outlives any browser context. +browser_session_id: ?[]const u8 = null, + browser_context: ?BrowserContext, // Re-used arena for processing a message. We're assuming that we're getting @@ -429,6 +436,11 @@ fn dispatchCommand(command: *Command, method: []const u8) !void { } fn isValidSessionId(self: *const CDP, input_session_id: []const u8) bool { + if (self.browser_session_id) |browser_session_id| { + if (std.mem.eql(u8, browser_session_id, input_session_id)) { + return true; + } + } const browser_context = &(self.browser_context orelse return false); const session_id = browser_context.session_id orelse return false; return std.mem.eql(u8, session_id, input_session_id); diff --git a/src/cdp/domains/target.zig b/src/cdp/domains/target.zig index c11ec2da0..049a022f0 100644 --- a/src/cdp/domains/target.zig +++ b/src/cdp/domains/target.zig @@ -254,7 +254,12 @@ fn attachToTarget(cmd: *CDP.Command) !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(); + const cdp = cmd.cdp; + // This session targets the browser, not the page. It must not touch + // bc.session_id: a non-null bc.session_id means "attached to the page + // target", and a browser-only attachment would break that invariant + // (e.g. the createTarget assert). + const session_id = cdp.browser_session_id orelse cdp.browser_session_id_gen.next(); try cmd.sendEvent("Target.attachedToTarget", AttachToTarget{ .sessionId = session_id, @@ -268,9 +273,9 @@ fn attachToBrowserTarget(cmd: *CDP.Command) !void { }, }, .{}); - bc.session_id = session_id; + cdp.browser_session_id = session_id; - return cmd.sendResult(.{ .sessionId = bc.session_id }, .{}); + return cmd.sendResult(.{ .sessionId = session_id }, .{}); } fn closeTarget(cmd: *CDP.Command) !void { @@ -655,6 +660,45 @@ test "cdp.target: createTarget" { } } +// A browser-target session (Target.attachToBrowserTarget) is distinct from +// the page-target session. It used to be stored in bc.session_id, which broke +// the "no target => no session_id" invariant asserted in createTarget. +test "cdp.target: attachToBrowserTarget then createTarget" { + var ctx = try testing.context(); + defer ctx.deinit(); + + try ctx.processMessage(.{ .id = 1, .method = "Target.createBrowserContext" }); + const bc = &ctx.cdp().browser_context.?; + try ctx.expectSentResult(.{ .browserContextId = bc.id }, .{ .id = 1 }); + + { + try ctx.processMessage(.{ .id = 2, .method = "Target.attachToBrowserTarget" }); + try ctx.expectSentEvent("Target.attachedToTarget", .{ .sessionId = "BSID-1", .targetInfo = .{ .targetId = bc.id, .title = "", .url = "", .attached = true, .type = "browser", .canAccessOpener = false } }, .{}); + try ctx.expectSentResult(.{ .sessionId = "BSID-1" }, .{ .id = 2 }); + + // the browser session must not occupy the page session slot + try testing.expectEqual(null, bc.session_id); + } + + { + // this used to fail the "not null session_id" assertion + try ctx.processMessage(.{ .id = 3, .method = "Target.createTarget", .params = .{ .url = "about:blank" } }); + try ctx.expectSentResult(.{ .targetId = bc.target_id.? }, .{ .id = 3 }); + } + + { + // the browser session id is valid on subsequent commands + try ctx.processMessage(.{ .id = 4, .method = "Target.setDiscoverTargets", .sessionId = "BSID-1", .params = .{ .discover = true } }); + try ctx.expectSentResult(null, .{ .id = 4, .session_id = "BSID-1" }); + } + + { + // unknown session ids are still rejected + try ctx.processMessage(.{ .id = 5, .method = "Target.setDiscoverTargets", .sessionId = "SID-NOPE", .params = .{ .discover = true } }); + try ctx.expectSentError(-32001, "Unknown sessionId", .{ .id = 5 }); + } +} + test "cdp.target: closeTarget" { var ctx = try testing.context(); defer ctx.deinit();