Compare commits

..

16 Commits

Author SHA1 Message Date
Adrià Arrufat
10b70a434e feat(cdp): emit Target.targetDestroyed when target is closed
This improves protocol compliance and helps automation frameworks properly clean up their internal state when targets are closed or replaced.
2026-03-30 11:23:45 +02:00
Adrià Arrufat
e354b7315f fix(cdp): auto-close existing target on createTarget
When creating a new target via `Target.createTarget`, any existing target in the browser context is now automatically closed and detached. This resolves an issue where automation frameworks (like Stagehand) would fail with a TargetAlreadyLoaded error when attempting to open multiple tabs sequentially.

Closes #1962
2026-03-30 11:07:31 +02:00
Pierre Tachoire
8723ecdd2d Merge pull request #2028 from lightpanda-io/http_client_safe_kill
Protect transfer.kill() the way transfer.abort() is protected
2026-03-30 09:22:40 +02:00
Pierre Tachoire
451178558a Merge pull request #2026 from lightpanda-io/invalid_access_dom_exception
Add missing InvalidAccessError DOMException mapping
2026-03-30 09:21:51 +02:00
Karl Seguin
70dc0f6b95 Merge pull request #2027 from lightpanda-io/mcp-protocol-version
mcp: allow configuring protocol version
2026-03-30 13:38:06 +08:00
Adrià Arrufat
d99599fa21 zig fmt 2026-03-30 07:24:08 +02:00
Adrià Arrufat
20e62a5551 mcp: inline mcpVersion helper from Config 2026-03-30 07:13:45 +02:00
Adrià Arrufat
e083d4a3d1 Config: remove LIGHTPANDA_MCP_VERSION env var 2026-03-30 07:07:23 +02:00
Karl Seguin
7a23686cbd Merge pull request #2033 from lightpanda-io/canvas_context_cache
Canvas context cache
2026-03-30 12:27:04 +08:00
Karl Seguin
25889ff918 Improve canvas context caching
Improve https://github.com/lightpanda-io/browser/pull/2022 to also cache webgl
context and add tests.
2026-03-30 12:14:32 +08:00
Karl Seguin
b4e3f246ca Merge remote-tracking branch 'evan108108/fix/canvas-getcontext-caching' into canvas_context_cache 2026-03-30 11:58:45 +08:00
Karl Seguin
f60e5cce6d Protect transfer.kill() the way transfer.abort() is protected
Transfer.abort() is protected from aborting the transfer while inside of a
libcurl callback (since libcurl doesn't support mutating the easy while inside
of a callback AND it causes issues in the zig code).

This applies similar logic to Transfer.kill() which is less likely to be called
but worse if it is called in a callback, as transfer.kill() deinit's the
transfer - something the callback caller is not expecting. Since killing isn't
safe to do, we flag the transfer as aborted AND null/noop all the callbacks.

Fixes WPT crash /content-security-policy/frame-src/frame-src-blocked-path-matching.sub.html
2026-03-29 19:48:47 +08:00
Adrià Arrufat
81d4bdb157 mcp: change default protocol version to 2024-11-05 2026-03-29 08:34:24 +02:00
Adrià Arrufat
cf5e4d7d1e mcp: allow configuring protocol version
Closes #2023
2026-03-29 08:29:04 +02:00
Karl Seguin
9f81d7d3ff Add missing InvalidAccessError DOMException mapping
Fixes WPT crash /WebCryptoAPI/sign_verify/eddsa_curve25519.https.any.html
2026-03-29 11:46:44 +08:00
evan108108
1f22462f13 fix: cache canvas 2D context and lock context type per spec
Per the HTML spec, HTMLCanvasElement.getContext() should:
1. Return the same object on repeated calls with the same type
2. Return null if a different context type was already requested

Previously, every getContext("2d") call created a new
CanvasRenderingContext2D object. This caused issues with code
that relies on identity checks (ctx === canvas.getContext("2d"))
and wasted memory by allocating duplicate contexts.

The fix caches the 2D context and tracks which context type was
first requested, returning null for incompatible subsequent calls.
2026-03-27 21:06:09 -04:00
29 changed files with 831 additions and 694 deletions

View File

@@ -24,6 +24,7 @@ const log = @import("log.zig");
const dump = @import("browser/dump.zig"); const dump = @import("browser/dump.zig");
const WebBotAuthConfig = @import("network/WebBotAuth.zig").Config; const WebBotAuthConfig = @import("network/WebBotAuth.zig").Config;
const mcp = @import("mcp.zig");
pub const RunMode = enum { pub const RunMode = enum {
help, help,
@@ -222,6 +223,7 @@ pub const Serve = struct {
pub const Mcp = struct { pub const Mcp = struct {
common: Common = .{}, common: Common = .{},
version: mcp.Version = .default,
}; };
pub const DumpFormat = enum { pub const DumpFormat = enum {
@@ -453,6 +455,12 @@ pub fn printUsageAndExit(self: *const Config, success: bool) void {
\\Starts an MCP (Model Context Protocol) server over stdio \\Starts an MCP (Model Context Protocol) server over stdio
\\Example: {s} mcp \\Example: {s} mcp
\\ \\
\\Options:
\\--version
\\ Override the reported MCP version.
\\ Valid: 2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25.
\\ Defaults to "2024-11-05".
\\
++ common_options ++ ++ common_options ++
\\ \\
\\version command \\version command
@@ -640,10 +648,22 @@ fn parseMcpArgs(
allocator: Allocator, allocator: Allocator,
args: *std.process.ArgIterator, args: *std.process.ArgIterator,
) !Mcp { ) !Mcp {
var mcp: Mcp = .{}; var result: Mcp = .{};
while (args.next()) |opt| { while (args.next()) |opt| {
if (try parseCommonArg(allocator, opt, args, &mcp.common)) { if (std.mem.eql(u8, "--version", opt)) {
const str = args.next() orelse {
log.fatal(.mcp, "missing argument value", .{ .arg = opt });
return error.InvalidArgument;
};
result.version = std.meta.stringToEnum(mcp.Version, str) orelse {
log.fatal(.mcp, "invalid protocol version", .{ .value = str });
return error.InvalidArgument;
};
continue;
}
if (try parseCommonArg(allocator, opt, args, &result.common)) {
continue; continue;
} }
@@ -651,7 +671,7 @@ fn parseMcpArgs(
return error.UnkownOption; return error.UnkownOption;
} }
return mcp; return result;
} }
fn parseFetchArgs( fn parseFetchArgs(

View File

@@ -821,16 +821,16 @@ fn processOneMessage(self: *Client, msg: http.Handles.MultiMessage, transfer: *T
break :blk std.ascii.eqlIgnoreCase(hdr.value, "close"); break :blk std.ascii.eqlIgnoreCase(hdr.value, "close");
}; };
if (msg.err != null and !is_conn_close_recv) {
transfer.requestFailed(transfer._callback_error orelse msg.err.?, true);
return true;
}
// make sure the transfer can't be immediately aborted from a callback // make sure the transfer can't be immediately aborted from a callback
// since we still need it here. // since we still need it here.
transfer._performing = true; transfer._performing = true;
defer transfer._performing = false; defer transfer._performing = false;
if (msg.err != null and !is_conn_close_recv) {
transfer.requestFailed(transfer._callback_error orelse msg.err.?, true);
return true;
}
if (!transfer._header_done_called) { if (!transfer._header_done_called) {
// In case of request w/o data, we need to call the header done // In case of request w/o data, we need to call the header done
// callback now. // callback now.
@@ -873,7 +873,6 @@ fn processMessages(self: *Client) !bool {
var processed = false; var processed = false;
while (self.handles.readMessage()) |msg| { while (self.handles.readMessage()) |msg| {
const transfer = try Transfer.fromConnection(&msg.conn); const transfer = try Transfer.fromConnection(&msg.conn);
const done = self.processOneMessage(msg, transfer) catch |err| blk: { const done = self.processOneMessage(msg, transfer) catch |err| blk: {
log.err(.http, "process_messages", .{ .err = err, .req = transfer }); log.err(.http, "process_messages", .{ .err = err, .req = transfer });
transfer.requestFailed(err, true); transfer.requestFailed(err, true);
@@ -1068,6 +1067,24 @@ pub const Transfer = struct {
if (self.req.shutdown_callback) |cb| { if (self.req.shutdown_callback) |cb| {
cb(self.ctx); cb(self.ctx);
} }
if (self._performing or self.client.performing) {
// We're currently inside of a callback. This client, and libcurl
// generally don't expect a transfer to become deinitialized during
// a callback. We can flag the transfer as aborted (which is what
// we do when transfer.abort() is called in this condition) AND,
// since this "kill()"should prevent any future callbacks, the best
// we can do is null/noop them.
self.aborted = true;
self.req.start_callback = null;
self.req.shutdown_callback = null;
self.req.header_callback = Noop.headerCallback;
self.req.data_callback = Noop.dataCallback;
self.req.done_callback = Noop.doneCallback;
self.req.error_callback = Noop.errorCallback;
return;
}
self.deinit(); self.deinit();
} }
@@ -1492,3 +1509,12 @@ pub const Transfer = struct {
return null; return null;
} }
}; };
const Noop = struct {
fn headerCallback(_: *Transfer) !bool {
return true;
}
fn dataCallback(_: *Transfer, _: []const u8) !void {}
fn doneCallback(_: *anyopaque) !void {}
fn errorCallback(_: *anyopaque, _: anyerror) void {}
};

View File

@@ -148,3 +148,13 @@
} }
</script> </script>
<script id=identity>
{
const element = document.createElement('canvas');
const ctx = element.getContext('2d');
testing.expectTrue(ctx === element.getContext('2d'));
testing.expectEqual(null, element.getContext('webgl'));
}
</script>

View File

@@ -85,3 +85,13 @@
loseContext.restoreContext(); loseContext.restoreContext();
} }
</script> </script>
<script id=identity>
{
const element = document.createElement('canvas');
const ctx = element.getContext('webgl');
testing.expectTrue(ctx === element.getContext('webgl'));
testing.expectEqual(null, element.getContext('2d'));
}
</script>

View File

@@ -59,6 +59,7 @@ pub fn fromError(err: anyerror) ?DOMException {
error.TimeoutError => .{ ._code = .timeout_error }, error.TimeoutError => .{ ._code = .timeout_error },
error.InvalidNodeType => .{ ._code = .invalid_node_type_error }, error.InvalidNodeType => .{ ._code = .invalid_node_type_error },
error.DataClone => .{ ._code = .data_clone_error }, error.DataClone => .{ ._code = .data_clone_error },
error.InvalidAccessError => .{ ._code = .invalid_access_error },
else => null, else => null,
}; };
} }

View File

@@ -29,6 +29,9 @@ const OffscreenCanvas = @import("../../canvas/OffscreenCanvas.zig");
const Canvas = @This(); const Canvas = @This();
_proto: *HtmlElement, _proto: *HtmlElement,
_cached: ?DrawingContext = null,
const ContextType = enum { none, @"2d", webgl };
pub fn asElement(self: *Canvas) *Element { pub fn asElement(self: *Canvas) *Element {
return self._proto._proto; return self._proto._proto;
@@ -68,17 +71,28 @@ const DrawingContext = union(enum) {
}; };
pub fn getContext(self: *Canvas, context_type: []const u8, page: *Page) !?DrawingContext { pub fn getContext(self: *Canvas, context_type: []const u8, page: *Page) !?DrawingContext {
if (self._cached) |cached| {
const matches = switch (cached) {
.@"2d" => std.mem.eql(u8, context_type, "2d"),
.webgl => std.mem.eql(u8, context_type, "webgl") or std.mem.eql(u8, context_type, "experimental-webgl"),
};
return if (matches) cached else null;
}
const drawing_context: DrawingContext = blk: {
if (std.mem.eql(u8, context_type, "2d")) { if (std.mem.eql(u8, context_type, "2d")) {
const ctx = try page._factory.create(CanvasRenderingContext2D{ ._canvas = self }); const ctx = try page._factory.create(CanvasRenderingContext2D{ ._canvas = self });
return .{ .@"2d" = ctx }; break :blk .{ .@"2d" = ctx };
} }
if (std.mem.eql(u8, context_type, "webgl") or std.mem.eql(u8, context_type, "experimental-webgl")) { if (std.mem.eql(u8, context_type, "webgl") or std.mem.eql(u8, context_type, "experimental-webgl")) {
const ctx = try page._factory.create(WebGLRenderingContext{}); const ctx = try page._factory.create(WebGLRenderingContext{});
return .{ .webgl = ctx }; break :blk .{ .webgl = ctx };
} }
return null; return null;
};
self._cached = drawing_context;
return drawing_context;
} }
/// Transfers control of the canvas to an OffscreenCanvas. /// Transfers control of the canvas to an OffscreenCanvas.

View File

@@ -63,7 +63,7 @@ target_id_gen: TargetIdGen = .{},
session_id_gen: SessionIdGen = .{}, session_id_gen: SessionIdGen = .{},
browser_context_id_gen: BrowserContextIdGen = .{}, browser_context_id_gen: BrowserContextIdGen = .{},
browser_context: ?BrowserContext, browser_context: ?BrowserContext(CDP),
// Re-used arena for processing a message. We're assuming that we're getting // Re-used arena for processing a message. We're assuming that we're getting
// 1 message at a time. // 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 { pub fn processMessage(self: *CDP, msg: []const u8) !void {
const arena = &self.message_arena; const arena = &self.message_arena;
defer _ = arena.reset(.{ .retain_with_limit = 1024 * 16 }); defer _ = arena.reset(.{ .retain_with_limit = 1024 * 16 });
return self.dispatch(arena.allocator(), .{ .cdp = self }, msg); return self.dispatch(arena.allocator(), self, msg);
} }
// @newhttp // @newhttp
@@ -136,12 +136,12 @@ pub fn pageWait(self: *CDP, ms: u32) !Session.Runner.CDPWaitResult {
// Called from above, in processMessage which handles client messages // Called from above, in processMessage which handles client messages
// but can also be called internally. For example, Target.sendMessageToTarget // but can also be called internally. For example, Target.sendMessageToTarget
// calls back into dispatch to capture the response. // calls back into dispatch to capture the response.
pub fn dispatch(self: *CDP, arena: Allocator, sender: Command.Sender, str: []const u8) !void { pub fn dispatch(self: *CDP, arena: Allocator, sender: anytype, str: []const u8) !void {
const input = json.parseFromSliceLeaky(InputMessage, arena, str, .{ const input = json.parseFromSliceLeaky(InputMessage, arena, str, .{
.ignore_unknown_fields = true, .ignore_unknown_fields = true,
}) catch return error.InvalidJSON; }) catch return error.InvalidJSON;
var command = Command{ var command = Command(CDP, @TypeOf(sender)){
.input = .{ .input = .{
.json = str, .json = str,
.id = input.id, .id = input.id,
@@ -186,7 +186,7 @@ pub fn dispatch(self: *CDP, arena: Allocator, sender: Command.Sender, str: []con
// "special" handling - the bare minimum we need to do until the driver // "special" handling - the bare minimum we need to do until the driver
// switches to a real BrowserContext. // switches to a real BrowserContext.
// (I can imagine this logic will become driver-specific) // (I can imagine this logic will become driver-specific)
fn dispatchStartupCommand(command: *Command, method: []const u8) !void { fn dispatchStartupCommand(command: anytype, method: []const u8) !void {
// Stagehand parses the response and error if we don't return a // Stagehand parses the response and error if we don't return a
// correct one for Page.getFrameTree on startup call. // correct one for Page.getFrameTree on startup call.
if (std.mem.eql(u8, method, "Page.getFrameTree")) { if (std.mem.eql(u8, method, "Page.getFrameTree")) {
@@ -197,7 +197,7 @@ fn dispatchStartupCommand(command: *Command, method: []const u8) !void {
return command.sendResult(null, .{}); return command.sendResult(null, .{});
} }
fn dispatchCommand(command: *Command, method: []const u8) !void { fn dispatchCommand(command: anytype, method: []const u8) !void {
const domain = blk: { const domain = blk: {
const i = std.mem.indexOfScalarPos(u8, method, 0, '.') orelse { const i = std.mem.indexOfScalarPos(u8, method, 0, '.') orelse {
return error.InvalidMethod; return error.InvalidMethod;
@@ -273,10 +273,10 @@ pub fn createBrowserContext(self: *CDP) ![]const u8 {
} }
const id = self.browser_context_id_gen.next(); const id = self.browser_context_id_gen.next();
self.browser_context = @as(BrowserContext, undefined); self.browser_context = @as(BrowserContext(CDP), undefined);
const browser_context = &self.browser_context.?; const browser_context = &self.browser_context.?;
try BrowserContext.init(browser_context, id, self); try BrowserContext(CDP).init(browser_context, id, self);
return id; return id;
} }
@@ -308,7 +308,7 @@ pub fn sendJSON(self: *CDP, message: anytype) !void {
}); });
} }
pub const BrowserContext = struct { pub fn BrowserContext(comptime CDP_T: type) type {
const Node = @import("Node.zig"); const Node = @import("Node.zig");
const AXNode = @import("AXNode.zig"); const AXNode = @import("AXNode.zig");
@@ -317,8 +317,9 @@ pub const BrowserContext = struct {
data: std.ArrayList(u8), data: std.ArrayList(u8),
}; };
return struct {
id: []const u8, id: []const u8,
cdp: *CDP, cdp: *CDP_T,
// Represents the browser session. There is no equivalent in CDP. For // Represents the browser session. There is no equivalent in CDP. For
// all intents and purpose, from CDP's point of view our Browser and // all intents and purpose, from CDP's point of view our Browser and
@@ -385,7 +386,9 @@ pub const BrowserContext = struct {
notification: *Notification, notification: *Notification,
fn init(self: *BrowserContext, id: []const u8, cdp: *CDP) !void { const Self = @This();
fn init(self: *Self, id: []const u8, cdp: *CDP_T) !void {
const allocator = cdp.allocator; const allocator = cdp.allocator;
// Create notification for this BrowserContext // Create notification for this BrowserContext
@@ -431,7 +434,7 @@ pub const BrowserContext = struct {
try notification.register(.page_frame_created, self, onPageFrameCreated); try notification.register(.page_frame_created, self, onPageFrameCreated);
} }
pub fn deinit(self: *BrowserContext) void { pub fn deinit(self: *Self) void {
const browser = &self.cdp.browser; const browser = &self.cdp.browser;
const env = &browser.env; const env = &browser.env;
@@ -477,12 +480,12 @@ pub const BrowserContext = struct {
self.intercept_state.deinit(); self.intercept_state.deinit();
} }
pub fn reset(self: *BrowserContext) void { pub fn reset(self: *Self) void {
self.node_registry.reset(); self.node_registry.reset();
self.node_search_list.reset(); self.node_search_list.reset();
} }
pub fn createIsolatedWorld(self: *BrowserContext, world_name: []const u8, grant_universal_access: bool) !*IsolatedWorld { pub fn createIsolatedWorld(self: *Self, world_name: []const u8, grant_universal_access: bool) !*IsolatedWorld {
const browser = &self.cdp.browser; const browser = &self.cdp.browser;
const arena = try browser.arena_pool.acquire(.{ .debug = "IsolatedWorld" }); const arena = try browser.arena_pool.acquire(.{ .debug = "IsolatedWorld" });
errdefer browser.arena_pool.release(arena); errdefer browser.arena_pool.release(arena);
@@ -505,7 +508,7 @@ pub const BrowserContext = struct {
return world; return world;
} }
pub fn nodeWriter(self: *BrowserContext, root: *const Node, opts: Node.Writer.Opts) Node.Writer { pub fn nodeWriter(self: *Self, root: *const Node, opts: Node.Writer.Opts) Node.Writer {
return .{ return .{
.root = root, .root = root,
.depth = opts.depth, .depth = opts.depth,
@@ -514,7 +517,7 @@ pub const BrowserContext = struct {
}; };
} }
pub fn axnodeWriter(self: *BrowserContext, root: *const Node, opts: AXNode.Writer.Opts) !AXNode.Writer { pub fn axnodeWriter(self: *Self, root: *const Node, opts: AXNode.Writer.Opts) !AXNode.Writer {
const page = self.session.currentPage() orelse return error.PageNotLoaded; const page = self.session.currentPage() orelse return error.PageNotLoaded;
_ = opts; _ = opts;
return .{ return .{
@@ -524,13 +527,13 @@ pub const BrowserContext = struct {
}; };
} }
pub fn getURL(self: *const BrowserContext) ?[:0]const u8 { pub fn getURL(self: *const Self) ?[:0]const u8 {
const page = self.session.currentPage() orelse return null; const page = self.session.currentPage() orelse return null;
const url = page.url; const url = page.url;
return if (url.len == 0) null else url; return if (url.len == 0) null else url;
} }
pub fn getTitle(self: *const BrowserContext) ?[]const u8 { pub fn getTitle(self: *const Self) ?[]const u8 {
const page = self.session.currentPage() orelse return null; const page = self.session.currentPage() orelse return null;
return page.getTitle() catch |err| { return page.getTitle() catch |err| {
log.err(.cdp, "page title", .{ .err = err }); log.err(.cdp, "page title", .{ .err = err });
@@ -538,7 +541,7 @@ pub const BrowserContext = struct {
}; };
} }
pub fn networkEnable(self: *BrowserContext) !void { pub fn networkEnable(self: *Self) !void {
try self.notification.register(.http_request_fail, self, onHttpRequestFail); try self.notification.register(.http_request_fail, self, onHttpRequestFail);
try self.notification.register(.http_request_start, self, onHttpRequestStart); try self.notification.register(.http_request_start, self, onHttpRequestStart);
try self.notification.register(.http_request_done, self, onHttpRequestDone); try self.notification.register(.http_request_done, self, onHttpRequestDone);
@@ -546,7 +549,7 @@ pub const BrowserContext = struct {
try self.notification.register(.http_response_header_done, self, onHttpResponseHeadersDone); try self.notification.register(.http_response_header_done, self, onHttpResponseHeadersDone);
} }
pub fn networkDisable(self: *BrowserContext) void { pub fn networkDisable(self: *Self) void {
self.notification.unregister(.http_request_fail, self); self.notification.unregister(.http_request_fail, self);
self.notification.unregister(.http_request_start, self); self.notification.unregister(.http_request_start, self);
self.notification.unregister(.http_request_done, self); self.notification.unregister(.http_request_done, self);
@@ -554,83 +557,83 @@ pub const BrowserContext = struct {
self.notification.unregister(.http_response_header_done, self); self.notification.unregister(.http_response_header_done, self);
} }
pub fn fetchEnable(self: *BrowserContext, authRequests: bool) !void { pub fn fetchEnable(self: *Self, authRequests: bool) !void {
try self.notification.register(.http_request_intercept, self, onHttpRequestIntercept); try self.notification.register(.http_request_intercept, self, onHttpRequestIntercept);
if (authRequests) { if (authRequests) {
try self.notification.register(.http_request_auth_required, self, onHttpRequestAuthRequired); try self.notification.register(.http_request_auth_required, self, onHttpRequestAuthRequired);
} }
} }
pub fn fetchDisable(self: *BrowserContext) void { pub fn fetchDisable(self: *Self) void {
self.notification.unregister(.http_request_intercept, self); self.notification.unregister(.http_request_intercept, self);
self.notification.unregister(.http_request_auth_required, self); self.notification.unregister(.http_request_auth_required, self);
} }
pub fn lifecycleEventsEnable(self: *BrowserContext) !void { pub fn lifecycleEventsEnable(self: *Self) !void {
self.page_life_cycle_events = true; self.page_life_cycle_events = true;
try self.notification.register(.page_network_idle, self, onPageNetworkIdle); try self.notification.register(.page_network_idle, self, onPageNetworkIdle);
try self.notification.register(.page_network_almost_idle, self, onPageNetworkAlmostIdle); try self.notification.register(.page_network_almost_idle, self, onPageNetworkAlmostIdle);
} }
pub fn lifecycleEventsDisable(self: *BrowserContext) void { pub fn lifecycleEventsDisable(self: *Self) void {
self.page_life_cycle_events = false; self.page_life_cycle_events = false;
self.notification.unregister(.page_network_idle, self); self.notification.unregister(.page_network_idle, self);
self.notification.unregister(.page_network_almost_idle, self); self.notification.unregister(.page_network_almost_idle, self);
} }
pub fn onPageRemove(ctx: *anyopaque, _: Notification.PageRemove) !void { pub fn onPageRemove(ctx: *anyopaque, _: Notification.PageRemove) !void {
const self: *BrowserContext = @ptrCast(@alignCast(ctx)); const self: *Self = @ptrCast(@alignCast(ctx));
try @import("domains/page.zig").pageRemove(self); try @import("domains/page.zig").pageRemove(self);
} }
pub fn onPageCreated(ctx: *anyopaque, page: *Page) !void { pub fn onPageCreated(ctx: *anyopaque, page: *Page) !void {
const self: *BrowserContext = @ptrCast(@alignCast(ctx)); const self: *Self = @ptrCast(@alignCast(ctx));
return @import("domains/page.zig").pageCreated(self, page); return @import("domains/page.zig").pageCreated(self, page);
} }
pub fn onPageNavigate(ctx: *anyopaque, msg: *const Notification.PageNavigate) !void { pub fn onPageNavigate(ctx: *anyopaque, msg: *const Notification.PageNavigate) !void {
const self: *BrowserContext = @ptrCast(@alignCast(ctx)); const self: *Self = @ptrCast(@alignCast(ctx));
return @import("domains/page.zig").pageNavigate(self, msg); return @import("domains/page.zig").pageNavigate(self, msg);
} }
pub fn onPageNavigated(ctx: *anyopaque, msg: *const Notification.PageNavigated) !void { pub fn onPageNavigated(ctx: *anyopaque, msg: *const Notification.PageNavigated) !void {
const self: *BrowserContext = @ptrCast(@alignCast(ctx)); const self: *Self = @ptrCast(@alignCast(ctx));
defer self.resetNotificationArena(); defer self.resetNotificationArena();
return @import("domains/page.zig").pageNavigated(self.notification_arena, self, msg); return @import("domains/page.zig").pageNavigated(self.notification_arena, self, msg);
} }
pub fn onPageFrameCreated(ctx: *anyopaque, msg: *const Notification.PageFrameCreated) !void { pub fn onPageFrameCreated(ctx: *anyopaque, msg: *const Notification.PageFrameCreated) !void {
const self: *BrowserContext = @ptrCast(@alignCast(ctx)); const self: *Self = @ptrCast(@alignCast(ctx));
return @import("domains/page.zig").pageFrameCreated(self, msg); return @import("domains/page.zig").pageFrameCreated(self, msg);
} }
pub fn onPageNetworkIdle(ctx: *anyopaque, msg: *const Notification.PageNetworkIdle) !void { pub fn onPageNetworkIdle(ctx: *anyopaque, msg: *const Notification.PageNetworkIdle) !void {
const self: *BrowserContext = @ptrCast(@alignCast(ctx)); const self: *Self = @ptrCast(@alignCast(ctx));
return @import("domains/page.zig").pageNetworkIdle(self, msg); return @import("domains/page.zig").pageNetworkIdle(self, msg);
} }
pub fn onPageNetworkAlmostIdle(ctx: *anyopaque, msg: *const Notification.PageNetworkAlmostIdle) !void { pub fn onPageNetworkAlmostIdle(ctx: *anyopaque, msg: *const Notification.PageNetworkAlmostIdle) !void {
const self: *BrowserContext = @ptrCast(@alignCast(ctx)); const self: *Self = @ptrCast(@alignCast(ctx));
return @import("domains/page.zig").pageNetworkAlmostIdle(self, msg); return @import("domains/page.zig").pageNetworkAlmostIdle(self, msg);
} }
pub fn onHttpRequestStart(ctx: *anyopaque, msg: *const Notification.RequestStart) !void { pub fn onHttpRequestStart(ctx: *anyopaque, msg: *const Notification.RequestStart) !void {
const self: *BrowserContext = @ptrCast(@alignCast(ctx)); const self: *Self = @ptrCast(@alignCast(ctx));
try @import("domains/network.zig").httpRequestStart(self, msg); try @import("domains/network.zig").httpRequestStart(self, msg);
} }
pub fn onHttpRequestIntercept(ctx: *anyopaque, msg: *const Notification.RequestIntercept) !void { pub fn onHttpRequestIntercept(ctx: *anyopaque, msg: *const Notification.RequestIntercept) !void {
const self: *BrowserContext = @ptrCast(@alignCast(ctx)); const self: *Self = @ptrCast(@alignCast(ctx));
try @import("domains/fetch.zig").requestIntercept(self, msg); try @import("domains/fetch.zig").requestIntercept(self, msg);
} }
pub fn onHttpRequestFail(ctx: *anyopaque, msg: *const Notification.RequestFail) !void { pub fn onHttpRequestFail(ctx: *anyopaque, msg: *const Notification.RequestFail) !void {
const self: *BrowserContext = @ptrCast(@alignCast(ctx)); const self: *Self = @ptrCast(@alignCast(ctx));
return @import("domains/network.zig").httpRequestFail(self, msg); return @import("domains/network.zig").httpRequestFail(self, msg);
} }
pub fn onHttpResponseHeadersDone(ctx: *anyopaque, msg: *const Notification.ResponseHeaderDone) !void { pub fn onHttpResponseHeadersDone(ctx: *anyopaque, msg: *const Notification.ResponseHeaderDone) !void {
const self: *BrowserContext = @ptrCast(@alignCast(ctx)); const self: *Self = @ptrCast(@alignCast(ctx));
defer self.resetNotificationArena(); defer self.resetNotificationArena();
const arena = self.page_arena; const arena = self.page_arena;
@@ -665,12 +668,12 @@ pub const BrowserContext = struct {
} }
pub fn onHttpRequestDone(ctx: *anyopaque, msg: *const Notification.RequestDone) !void { pub fn onHttpRequestDone(ctx: *anyopaque, msg: *const Notification.RequestDone) !void {
const self: *BrowserContext = @ptrCast(@alignCast(ctx)); const self: *Self = @ptrCast(@alignCast(ctx));
return @import("domains/network.zig").httpRequestDone(self, msg); return @import("domains/network.zig").httpRequestDone(self, msg);
} }
pub fn onHttpResponseData(ctx: *anyopaque, msg: *const Notification.ResponseData) !void { pub fn onHttpResponseData(ctx: *anyopaque, msg: *const Notification.ResponseData) !void {
const self: *BrowserContext = @ptrCast(@alignCast(ctx)); const self: *Self = @ptrCast(@alignCast(ctx));
const arena = self.page_arena; const arena = self.page_arena;
const id = msg.transfer.id; const id = msg.transfer.id;
@@ -680,16 +683,16 @@ pub const BrowserContext = struct {
} }
pub fn onHttpRequestAuthRequired(ctx: *anyopaque, data: *const Notification.RequestAuthRequired) !void { pub fn onHttpRequestAuthRequired(ctx: *anyopaque, data: *const Notification.RequestAuthRequired) !void {
const self: *BrowserContext = @ptrCast(@alignCast(ctx)); const self: *Self = @ptrCast(@alignCast(ctx));
defer self.resetNotificationArena(); defer self.resetNotificationArena();
try @import("domains/fetch.zig").requestAuthRequired(self, data); try @import("domains/fetch.zig").requestAuthRequired(self, data);
} }
fn resetNotificationArena(self: *BrowserContext) void { fn resetNotificationArena(self: *Self) void {
defer _ = self.cdp.notification_arena.reset(.{ .retain_with_limit = 1024 * 64 }); defer _ = self.cdp.notification_arena.reset(.{ .retain_with_limit = 1024 * 64 });
} }
pub fn callInspector(self: *const BrowserContext, msg: []const u8) void { pub fn callInspector(self: *const Self, msg: []const u8) void {
self.inspector_session.send(msg); self.inspector_session.send(msg);
self.session.browser.env.runMicrotasks(); self.session.browser.env.runMicrotasks();
} }
@@ -720,7 +723,7 @@ pub const BrowserContext = struct {
// This is hacky x 2. First, we create the JSON payload by gluing our // 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 // session_id onto it. Second, we're much more client/websocket aware than
// we should be. // we should be.
fn sendInspectorMessage(self: *BrowserContext, msg: []const u8) !void { fn sendInspectorMessage(self: *Self, msg: []const u8) !void {
const session_id = self.session_id orelse { const session_id = self.session_id orelse {
// We no longer have an active session. What should we do // We no longer have an active session. What should we do
// in this case? // in this case?
@@ -756,7 +759,8 @@ pub const BrowserContext = struct {
try cdp.client.sendJSONRaw(buf); try cdp.client.sendJSONRaw(buf);
} }
}; };
}
/// see: https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/bindings/core/v8/V8BindingDesign.md#world /// 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. /// The current understanding. An isolated world lives in the same isolate, but a separated context.
@@ -828,16 +832,17 @@ const IsolatedWorld = struct {
// behaviors. Normally, we're sending the result to the client. But in some cases // 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 // we want to capture the result. So we want the command.sendResult to be
// generic. // generic.
pub const Command = struct { 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 // A misc arena that can be used for any allocation for processing
// the message // the message
arena: Allocator, arena: Allocator,
// reference to our CDP instance // reference to our CDP instance
cdp: *CDP, cdp: *CDP_T,
// The browser context this command targets // The browser context this command targets
browser_context: ?*BrowserContext, browser_context: ?*BrowserContext(CDP_T),
// The command input (the id, optional session_id, params, ...) // The command input (the id, optional session_id, params, ...)
input: Input, input: Input,
@@ -848,23 +853,9 @@ pub const Command = struct {
// be code to capture the data that we were "sending". // be code to capture the data that we were "sending".
sender: Sender, sender: Sender,
const Sender = union(enum) { const Self = @This();
cdp: *CDP,
capture: *std.Io.Writer,
pub fn sendJSON(self: Sender, message: anytype) !void { pub fn params(self: *const Self, comptime T: type) !?T {
switch (self) {
.cdp => |cdp| return cdp.sendJSON(message),
.capture => |writer| {
return std.json.Stringify.value(message, .{
.emit_null_optional_fields = false,
}, writer);
},
}
}
};
pub fn params(self: *const Command, comptime T: type) !?T {
if (self.input.params) |p| { if (self.input.params) |p| {
return try json.parseFromSliceLeaky( return try json.parseFromSliceLeaky(
T, T,
@@ -876,7 +867,7 @@ pub const Command = struct {
return null; return null;
} }
pub fn createBrowserContext(self: *Command) !*BrowserContext { pub fn createBrowserContext(self: *Self) !*BrowserContext(CDP_T) {
_ = try self.cdp.createBrowserContext(); _ = try self.cdp.createBrowserContext();
self.browser_context = &(self.cdp.browser_context.?); self.browser_context = &(self.cdp.browser_context.?);
return self.browser_context.?; return self.browser_context.?;
@@ -885,7 +876,7 @@ pub const Command = struct {
const SendResultOpts = struct { const SendResultOpts = struct {
include_session_id: bool = true, include_session_id: bool = true,
}; };
pub fn sendResult(self: *Command, result: anytype, opts: SendResultOpts) !void { pub fn sendResult(self: *Self, result: anytype, opts: SendResultOpts) !void {
return self.sender.sendJSON(.{ return self.sender.sendJSON(.{
.id = self.input.id, .id = self.input.id,
.result = if (comptime @typeInfo(@TypeOf(result)) == .null) struct {}{} else result, .result = if (comptime @typeInfo(@TypeOf(result)) == .null) struct {}{} else result,
@@ -893,7 +884,10 @@ pub const Command = struct {
}); });
} }
pub fn sendEvent(self: *Command, method: []const u8, p: anytype, opts: SendEventOpts) !void { 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 // Events ALWAYS go to the client. self.sender should not be used
return self.cdp.sendEvent(method, p, opts); return self.cdp.sendEvent(method, p, opts);
} }
@@ -901,7 +895,7 @@ pub const Command = struct {
const SendErrorOpts = struct { const SendErrorOpts = struct {
include_session_id: bool = true, include_session_id: bool = true,
}; };
pub fn sendError(self: *Command, code: i32, message: []const u8, opts: SendErrorOpts) !void { pub fn sendError(self: *Self, code: i32, message: []const u8, opts: SendErrorOpts) !void {
return self.sender.sendJSON(.{ return self.sender.sendJSON(.{
.id = self.input.id, .id = self.input.id,
.@"error" = .{ .code = code, .message = message }, .@"error" = .{ .code = code, .message = message },
@@ -926,7 +920,8 @@ pub const Command = struct {
// The full raw json input // The full raw json input
json: []const u8, json: []const u8,
}; };
}; };
}
// When we parse a JSON message from the client, this is the structure // When we parse a JSON message from the client, this is the structure
// we always expect // we always expect

View File

@@ -18,9 +18,8 @@
const std = @import("std"); const std = @import("std");
const id = @import("../id.zig"); const id = @import("../id.zig");
const CDP = @import("../CDP.zig");
pub fn processMessage(cmd: *CDP.Command) !void { pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum { const action = std.meta.stringToEnum(enum {
enable, enable,
disable, disable,
@@ -33,15 +32,15 @@ pub fn processMessage(cmd: *CDP.Command) !void {
.getFullAXTree => return getFullAXTree(cmd), .getFullAXTree => return getFullAXTree(cmd),
} }
} }
fn enable(cmd: *CDP.Command) !void { fn enable(cmd: anytype) !void {
return cmd.sendResult(null, .{}); return cmd.sendResult(null, .{});
} }
fn disable(cmd: *CDP.Command) !void { fn disable(cmd: anytype) !void {
return cmd.sendResult(null, .{}); return cmd.sendResult(null, .{});
} }
fn getFullAXTree(cmd: *CDP.Command) !void { fn getFullAXTree(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
depth: ?i32 = null, depth: ?i32 = null,
frameId: ?[]const u8 = null, frameId: ?[]const u8 = null,

View File

@@ -17,7 +17,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const CDP = @import("../CDP.zig");
// TODO: hard coded data // TODO: hard coded data
const PROTOCOL_VERSION = "1.3"; const PROTOCOL_VERSION = "1.3";
@@ -36,7 +35,7 @@ const PRODUCT = "Chrome/124.0.6367.29";
const JS_VERSION = "12.4.254.8"; const JS_VERSION = "12.4.254.8";
const DEV_TOOLS_WINDOW_ID = 1923710101; const DEV_TOOLS_WINDOW_ID = 1923710101;
pub fn processMessage(cmd: *CDP.Command) !void { pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum { const action = std.meta.stringToEnum(enum {
getVersion, getVersion,
setPermission, setPermission,
@@ -58,7 +57,7 @@ pub fn processMessage(cmd: *CDP.Command) !void {
} }
} }
fn getVersion(cmd: *CDP.Command) !void { fn getVersion(cmd: anytype) !void {
// TODO: pre-serialize? // TODO: pre-serialize?
return cmd.sendResult(.{ return cmd.sendResult(.{
.protocolVersion = PROTOCOL_VERSION, .protocolVersion = PROTOCOL_VERSION,
@@ -70,7 +69,7 @@ fn getVersion(cmd: *CDP.Command) !void {
} }
// TODO: noop method // TODO: noop method
fn setDownloadBehavior(cmd: *CDP.Command) !void { fn setDownloadBehavior(cmd: anytype) !void {
// const params = (try cmd.params(struct { // const params = (try cmd.params(struct {
// behavior: []const u8, // behavior: []const u8,
// browserContextId: ?[]const u8 = null, // browserContextId: ?[]const u8 = null,
@@ -81,7 +80,7 @@ fn setDownloadBehavior(cmd: *CDP.Command) !void {
return cmd.sendResult(null, .{ .include_session_id = false }); return cmd.sendResult(null, .{ .include_session_id = false });
} }
fn getWindowForTarget(cmd: *CDP.Command) !void { fn getWindowForTarget(cmd: anytype) !void {
// const params = (try cmd.params(struct { // const params = (try cmd.params(struct {
// targetId: ?[]const u8 = null, // targetId: ?[]const u8 = null,
// })) orelse return error.InvalidParams; // })) orelse return error.InvalidParams;
@@ -92,22 +91,22 @@ fn getWindowForTarget(cmd: *CDP.Command) !void {
} }
// TODO: noop method // TODO: noop method
fn setWindowBounds(cmd: *CDP.Command) !void { fn setWindowBounds(cmd: anytype) !void {
return cmd.sendResult(null, .{}); return cmd.sendResult(null, .{});
} }
// TODO: noop method // TODO: noop method
fn grantPermissions(cmd: *CDP.Command) !void { fn grantPermissions(cmd: anytype) !void {
return cmd.sendResult(null, .{}); return cmd.sendResult(null, .{});
} }
// TODO: noop method // TODO: noop method
fn setPermission(cmd: *CDP.Command) !void { fn setPermission(cmd: anytype) !void {
return cmd.sendResult(null, .{}); return cmd.sendResult(null, .{});
} }
// TODO: noop method // TODO: noop method
fn resetPermissions(cmd: *CDP.Command) !void { fn resetPermissions(cmd: anytype) !void {
return cmd.sendResult(null, .{}); return cmd.sendResult(null, .{});
} }

View File

@@ -17,9 +17,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const CDP = @import("../CDP.zig");
pub fn processMessage(cmd: *CDP.Command) !void { pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum { const action = std.meta.stringToEnum(enum {
enable, enable,
}, cmd.input.action) orelse return error.UnknownMethod; }, cmd.input.action) orelse return error.UnknownMethod;

View File

@@ -18,18 +18,17 @@
const std = @import("std"); const std = @import("std");
const id = @import("../id.zig"); const id = @import("../id.zig");
const CDP = @import("../CDP.zig");
const Node = @import("../Node.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const dump = @import("../../browser/dump.zig"); const Node = @import("../Node.zig");
const js = @import("../../browser/js/js.zig");
const DOMNode = @import("../../browser/webapi/Node.zig"); const DOMNode = @import("../../browser/webapi/Node.zig");
const Selector = @import("../../browser/webapi/selector/Selector.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; const Allocator = std.mem.Allocator;
pub fn processMessage(cmd: *CDP.Command) !void { pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum { const action = std.meta.stringToEnum(enum {
enable, enable,
getDocument, getDocument,
@@ -70,7 +69,7 @@ pub fn processMessage(cmd: *CDP.Command) !void {
} }
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getDocument // https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getDocument
fn getDocument(cmd: *CDP.Command) !void { fn getDocument(cmd: anytype) !void {
const Params = struct { const Params = struct {
// CDP documentation implies that 0 isn't valid, but it _does_ work in Chrome // CDP documentation implies that 0 isn't valid, but it _does_ work in Chrome
depth: i32 = 3, depth: i32 = 3,
@@ -90,7 +89,7 @@ fn getDocument(cmd: *CDP.Command) !void {
} }
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-performSearch // https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-performSearch
fn performSearch(cmd: *CDP.Command) !void { fn performSearch(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
query: []const u8, query: []const u8,
includeUserAgentShadowDOM: ?bool = null, includeUserAgentShadowDOM: ?bool = null,
@@ -117,7 +116,7 @@ fn performSearch(cmd: *CDP.Command) !void {
// hierarchy of each nodes. // hierarchy of each nodes.
// We dispatch event in the reverse order: from the top level to the direct parents. // 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. // We should dispatch a node only if it has never been sent.
fn dispatchSetChildNodes(cmd: *CDP.Command, dom_nodes: []const *DOMNode) !void { fn dispatchSetChildNodes(cmd: anytype, dom_nodes: []const *DOMNode) !void {
const arena = cmd.arena; const arena = cmd.arena;
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const session_id = bc.session_id orelse return error.SessionIdNotLoaded; const session_id = bc.session_id orelse return error.SessionIdNotLoaded;
@@ -173,7 +172,7 @@ fn dispatchSetChildNodes(cmd: *CDP.Command, dom_nodes: []const *DOMNode) !void {
} }
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-discardSearchResults // https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-discardSearchResults
fn discardSearchResults(cmd: *CDP.Command) !void { fn discardSearchResults(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
searchId: []const u8, searchId: []const u8,
})) orelse return error.InvalidParams; })) orelse return error.InvalidParams;
@@ -185,7 +184,7 @@ fn discardSearchResults(cmd: *CDP.Command) !void {
} }
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getSearchResults // https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getSearchResults
fn getSearchResults(cmd: *CDP.Command) !void { fn getSearchResults(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
searchId: []const u8, searchId: []const u8,
fromIndex: u32, fromIndex: u32,
@@ -210,7 +209,7 @@ fn getSearchResults(cmd: *CDP.Command) !void {
return cmd.sendResult(.{ .nodeIds = node_ids[params.fromIndex..params.toIndex] }, .{}); return cmd.sendResult(.{ .nodeIds = node_ids[params.fromIndex..params.toIndex] }, .{});
} }
fn querySelector(cmd: *CDP.Command) !void { fn querySelector(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
nodeId: Node.Id, nodeId: Node.Id,
selector: []const u8, selector: []const u8,
@@ -236,7 +235,7 @@ fn querySelector(cmd: *CDP.Command) !void {
}, .{}); }, .{});
} }
fn querySelectorAll(cmd: *CDP.Command) !void { fn querySelectorAll(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
nodeId: Node.Id, nodeId: Node.Id,
selector: []const u8, selector: []const u8,
@@ -267,7 +266,7 @@ fn querySelectorAll(cmd: *CDP.Command) !void {
}, .{}); }, .{});
} }
fn resolveNode(cmd: *CDP.Command) !void { fn resolveNode(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
nodeId: ?Node.Id = null, nodeId: ?Node.Id = null,
backendNodeId: ?u32 = null, backendNodeId: ?u32 = null,
@@ -328,7 +327,7 @@ fn resolveNode(cmd: *CDP.Command) !void {
} }, .{}); } }, .{});
} }
fn describeNode(cmd: *CDP.Command) !void { fn describeNode(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
nodeId: ?Node.Id = null, nodeId: ?Node.Id = null,
backendNodeId: ?Node.Id = null, backendNodeId: ?Node.Id = null,
@@ -375,7 +374,7 @@ fn rectToQuad(rect: DOMNode.Element.DOMRect) Quad {
}; };
} }
fn scrollIntoViewIfNeeded(cmd: *CDP.Command) !void { fn scrollIntoViewIfNeeded(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
nodeId: ?Node.Id = null, nodeId: ?Node.Id = null,
backendNodeId: ?u32 = null, backendNodeId: ?u32 = null,
@@ -398,7 +397,7 @@ fn scrollIntoViewIfNeeded(cmd: *CDP.Command) !void {
return cmd.sendResult(null, .{}); return cmd.sendResult(null, .{});
} }
fn getNode(arena: Allocator, bc: *CDP.BrowserContext, node_id: ?Node.Id, backend_node_id: ?Node.Id, object_id: ?[]const u8) !*Node { fn getNode(arena: Allocator, bc: anytype, node_id: ?Node.Id, backend_node_id: ?Node.Id, object_id: ?[]const u8) !*Node {
const input_node_id = node_id orelse backend_node_id; const input_node_id = node_id orelse backend_node_id;
if (input_node_id) |input_node_id_| { if (input_node_id) |input_node_id_| {
return bc.node_registry.lookup_by_id.get(input_node_id_) orelse return error.NodeNotFound; return bc.node_registry.lookup_by_id.get(input_node_id_) orelse return error.NodeNotFound;
@@ -418,7 +417,7 @@ fn getNode(arena: Allocator, bc: *CDP.BrowserContext, node_id: ?Node.Id, backend
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getContentQuads // https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getContentQuads
// Related to: https://drafts.csswg.org/cssom-view/#the-geometryutils-interface // Related to: https://drafts.csswg.org/cssom-view/#the-geometryutils-interface
fn getContentQuads(cmd: *CDP.Command) !void { fn getContentQuads(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
nodeId: ?Node.Id = null, nodeId: ?Node.Id = null,
backendNodeId: ?Node.Id = null, backendNodeId: ?Node.Id = null,
@@ -444,7 +443,7 @@ fn getContentQuads(cmd: *CDP.Command) !void {
return cmd.sendResult(.{ .quads = &.{quad} }, .{}); return cmd.sendResult(.{ .quads = &.{quad} }, .{});
} }
fn getBoxModel(cmd: *CDP.Command) !void { fn getBoxModel(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
nodeId: ?Node.Id = null, nodeId: ?Node.Id = null,
backendNodeId: ?u32 = null, backendNodeId: ?u32 = null,
@@ -473,7 +472,7 @@ fn getBoxModel(cmd: *CDP.Command) !void {
} }, .{}); } }, .{});
} }
fn requestChildNodes(cmd: *CDP.Command) !void { fn requestChildNodes(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
nodeId: Node.Id, nodeId: Node.Id,
depth: i32 = 1, depth: i32 = 1,
@@ -497,7 +496,7 @@ fn requestChildNodes(cmd: *CDP.Command) !void {
return cmd.sendResult(null, .{}); return cmd.sendResult(null, .{});
} }
fn getFrameOwner(cmd: *CDP.Command) !void { fn getFrameOwner(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
frameId: []const u8, frameId: []const u8,
})) orelse return error.InvalidParams; })) orelse return error.InvalidParams;
@@ -513,7 +512,7 @@ fn getFrameOwner(cmd: *CDP.Command) !void {
return cmd.sendResult(.{ .nodeId = node.id, .backendNodeId = node.id }, .{}); return cmd.sendResult(.{ .nodeId = node.id, .backendNodeId = node.id }, .{});
} }
fn getOuterHTML(cmd: *CDP.Command) !void { fn getOuterHTML(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
nodeId: ?Node.Id = null, nodeId: ?Node.Id = null,
backendNodeId: ?Node.Id = null, backendNodeId: ?Node.Id = null,
@@ -535,7 +534,7 @@ fn getOuterHTML(cmd: *CDP.Command) !void {
return cmd.sendResult(.{ .outerHTML = aw.written() }, .{}); return cmd.sendResult(.{ .outerHTML = aw.written() }, .{});
} }
fn requestNode(cmd: *CDP.Command) !void { fn requestNode(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
objectId: []const u8, objectId: []const u8,
})) orelse return error.InvalidParams; })) orelse return error.InvalidParams;

View File

@@ -17,10 +17,9 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const CDP = @import("../CDP.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
pub fn processMessage(cmd: *CDP.Command) !void { pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum { const action = std.meta.stringToEnum(enum {
setEmulatedMedia, setEmulatedMedia,
setFocusEmulationEnabled, setFocusEmulationEnabled,
@@ -39,7 +38,7 @@ pub fn processMessage(cmd: *CDP.Command) !void {
} }
// TODO: noop method // TODO: noop method
fn setEmulatedMedia(cmd: *CDP.Command) !void { fn setEmulatedMedia(cmd: anytype) !void {
// const input = (try const incoming.params(struct { // const input = (try const incoming.params(struct {
// media: ?[]const u8 = null, // media: ?[]const u8 = null,
// features: ?[]struct{ // features: ?[]struct{
@@ -52,7 +51,7 @@ fn setEmulatedMedia(cmd: *CDP.Command) !void {
} }
// TODO: noop method // TODO: noop method
fn setFocusEmulationEnabled(cmd: *CDP.Command) !void { fn setFocusEmulationEnabled(cmd: anytype) !void {
// const input = (try const incoming.params(struct { // const input = (try const incoming.params(struct {
// enabled: bool, // enabled: bool,
// })) orelse return error.InvalidParams; // })) orelse return error.InvalidParams;
@@ -60,16 +59,16 @@ fn setFocusEmulationEnabled(cmd: *CDP.Command) !void {
} }
// TODO: noop method // TODO: noop method
fn setDeviceMetricsOverride(cmd: *CDP.Command) !void { fn setDeviceMetricsOverride(cmd: anytype) !void {
return cmd.sendResult(null, .{}); return cmd.sendResult(null, .{});
} }
// TODO: noop method // TODO: noop method
fn setTouchEmulationEnabled(cmd: *CDP.Command) !void { fn setTouchEmulationEnabled(cmd: anytype) !void {
return cmd.sendResult(null, .{}); return cmd.sendResult(null, .{});
} }
fn setUserAgentOverride(cmd: *CDP.Command) !void { fn setUserAgentOverride(cmd: anytype) !void {
log.info(.app, "setUserAgentOverride ignored", .{}); log.info(.app, "setUserAgentOverride ignored", .{});
return cmd.sendResult(null, .{}); return cmd.sendResult(null, .{});
} }

View File

@@ -17,19 +17,17 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const Allocator = std.mem.Allocator;
const id = @import("../id.zig"); const id = @import("../id.zig");
const CDP = @import("../CDP.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const network = @import("network.zig");
const HttpClient = @import("../../browser/HttpClient.zig"); const HttpClient = @import("../../browser/HttpClient.zig");
const net_http = @import("../../network/http.zig"); const net_http = @import("../../network/http.zig");
const Notification = @import("../../Notification.zig"); const Notification = @import("../../Notification.zig");
const network = @import("network.zig"); pub fn processMessage(cmd: anytype) !void {
const Allocator = std.mem.Allocator;
pub fn processMessage(cmd: *CDP.Command) !void {
const action = std.meta.stringToEnum(enum { const action = std.meta.stringToEnum(enum {
disable, disable,
enable, enable,
@@ -137,13 +135,13 @@ const ErrorReason = enum {
BlockedByResponse, BlockedByResponse,
}; };
fn disable(cmd: *CDP.Command) !void { fn disable(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
bc.fetchDisable(); bc.fetchDisable();
return cmd.sendResult(null, .{}); return cmd.sendResult(null, .{});
} }
fn enable(cmd: *CDP.Command) !void { fn enable(cmd: anytype) !void {
const params = (try cmd.params(EnableParam)) orelse EnableParam{}; const params = (try cmd.params(EnableParam)) orelse EnableParam{};
if (!arePatternsSupported(params.patterns)) { if (!arePatternsSupported(params.patterns)) {
log.warn(.not_implemented, "Fetch.enable", .{ .params = "pattern" }); log.warn(.not_implemented, "Fetch.enable", .{ .params = "pattern" });
@@ -182,7 +180,7 @@ fn arePatternsSupported(patterns: []RequestPattern) bool {
return true; return true;
} }
pub fn requestIntercept(bc: *CDP.BrowserContext, 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 // detachTarget could be called, in which case, we still have a page doing
// things, but no session. // things, but no session.
const session_id = bc.session_id orelse return; const session_id = bc.session_id orelse return;
@@ -217,7 +215,7 @@ pub fn requestIntercept(bc: *CDP.BrowserContext, intercept: *const Notification.
intercept.wait_for_interception.* = true; intercept.wait_for_interception.* = true;
} }
fn continueRequest(cmd: *CDP.Command) !void { fn continueRequest(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
requestId: []const u8, // INT-{d}" requestId: []const u8, // INT-{d}"
@@ -277,7 +275,7 @@ const AuthChallengeResponse = enum {
ProvideCredentials, ProvideCredentials,
}; };
fn continueWithAuth(cmd: *CDP.Command) !void { fn continueWithAuth(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
requestId: []const u8, // "INT-{d}" requestId: []const u8, // "INT-{d}"
@@ -320,7 +318,7 @@ fn continueWithAuth(cmd: *CDP.Command) !void {
return cmd.sendResult(null, .{}); return cmd.sendResult(null, .{});
} }
fn fulfillRequest(cmd: *CDP.Command) !void { fn fulfillRequest(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
@@ -362,7 +360,7 @@ fn fulfillRequest(cmd: *CDP.Command) !void {
return cmd.sendResult(null, .{}); return cmd.sendResult(null, .{});
} }
fn failRequest(cmd: *CDP.Command) !void { fn failRequest(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
requestId: []const u8, // "INT-{d}" requestId: []const u8, // "INT-{d}"
@@ -384,7 +382,7 @@ fn failRequest(cmd: *CDP.Command) !void {
return cmd.sendResult(null, .{}); return cmd.sendResult(null, .{});
} }
pub fn requestAuthRequired(bc: *CDP.BrowserContext, 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 // detachTarget could be called, in which case, we still have a page doing
// things, but no session. // things, but no session.
const session_id = bc.session_id orelse return; const session_id = bc.session_id orelse return;

View File

@@ -17,9 +17,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const CDP = @import("../CDP.zig");
pub fn processMessage(cmd: *CDP.Command) !void { pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum { const action = std.meta.stringToEnum(enum {
dispatchKeyEvent, dispatchKeyEvent,
dispatchMouseEvent, dispatchMouseEvent,
@@ -34,7 +33,7 @@ pub fn processMessage(cmd: *CDP.Command) !void {
} }
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchKeyEvent // https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchKeyEvent
fn dispatchKeyEvent(cmd: *CDP.Command) !void { fn dispatchKeyEvent(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
type: Type, type: Type,
key: []const u8 = "", key: []const u8 = "",
@@ -75,7 +74,7 @@ fn dispatchKeyEvent(cmd: *CDP.Command) !void {
} }
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchMouseEvent // https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchMouseEvent
fn dispatchMouseEvent(cmd: *CDP.Command) !void { fn dispatchMouseEvent(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
x: f64, x: f64,
y: f64, y: f64,
@@ -105,7 +104,7 @@ fn dispatchMouseEvent(cmd: *CDP.Command) !void {
} }
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-insertText // https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-insertText
fn insertText(cmd: *CDP.Command) !void { fn insertText(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
text: []const u8, // The text to insert text: []const u8, // The text to insert
})) orelse return error.InvalidParams; })) orelse return error.InvalidParams;

View File

@@ -17,9 +17,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const CDP = @import("../CDP.zig");
pub fn processMessage(cmd: *CDP.Command) !void { pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum { const action = std.meta.stringToEnum(enum {
enable, enable,
disable, disable,

View File

@@ -17,9 +17,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const CDP = @import("../CDP.zig");
pub fn processMessage(cmd: *CDP.Command) !void { pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum { const action = std.meta.stringToEnum(enum {
enable, enable,
disable, disable,

View File

@@ -18,18 +18,14 @@
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda"); 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 markdown = lp.markdown;
const SemanticTree = lp.SemanticTree; const SemanticTree = lp.SemanticTree;
const interactive = lp.interactive; const interactive = lp.interactive;
const structured_data = lp.structured_data; const structured_data = lp.structured_data;
const Node = @import("../Node.zig");
const DOMNode = @import("../../browser/webapi/Node.zig");
pub fn processMessage(cmd: *CDP.Command) !void { pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum { const action = std.meta.stringToEnum(enum {
getMarkdown, getMarkdown,
getSemanticTree, getSemanticTree,
@@ -55,7 +51,7 @@ pub fn processMessage(cmd: *CDP.Command) !void {
} }
} }
fn getSemanticTree(cmd: *CDP.Command) !void { fn getSemanticTree(cmd: anytype) !void {
const Params = struct { const Params = struct {
format: ?enum { text } = null, format: ?enum { text } = null,
prune: ?bool = null, prune: ?bool = null,
@@ -100,7 +96,7 @@ fn getSemanticTree(cmd: *CDP.Command) !void {
}, .{}); }, .{});
} }
fn getMarkdown(cmd: *CDP.Command) !void { fn getMarkdown(cmd: anytype) !void {
const Params = struct { const Params = struct {
nodeId: ?Node.Id = null, nodeId: ?Node.Id = null,
}; };
@@ -123,7 +119,7 @@ fn getMarkdown(cmd: *CDP.Command) !void {
}, .{}); }, .{});
} }
fn getInteractiveElements(cmd: *CDP.Command) !void { fn getInteractiveElements(cmd: anytype) !void {
const Params = struct { const Params = struct {
nodeId: ?Node.Id = null, nodeId: ?Node.Id = null,
}; };
@@ -145,7 +141,7 @@ fn getInteractiveElements(cmd: *CDP.Command) !void {
}, .{}); }, .{});
} }
fn getStructuredData(cmd: *CDP.Command) !void { fn getStructuredData(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.NoBrowserContext; const bc = cmd.browser_context orelse return error.NoBrowserContext;
const page = bc.session.currentPage() orelse return error.PageNotLoaded; const page = bc.session.currentPage() orelse return error.PageNotLoaded;
@@ -160,7 +156,7 @@ fn getStructuredData(cmd: *CDP.Command) !void {
}, .{}); }, .{});
} }
fn detectForms(cmd: *CDP.Command) !void { fn detectForms(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.NoBrowserContext; const bc = cmd.browser_context orelse return error.NoBrowserContext;
const page = bc.session.currentPage() orelse return error.PageNotLoaded; const page = bc.session.currentPage() orelse return error.PageNotLoaded;
@@ -177,7 +173,7 @@ fn detectForms(cmd: *CDP.Command) !void {
}, .{}); }, .{});
} }
fn clickNode(cmd: *CDP.Command) !void { fn clickNode(cmd: anytype) !void {
const Params = struct { const Params = struct {
nodeId: ?Node.Id = null, nodeId: ?Node.Id = null,
backendNodeId: ?Node.Id = null, backendNodeId: ?Node.Id = null,
@@ -198,7 +194,7 @@ fn clickNode(cmd: *CDP.Command) !void {
return cmd.sendResult(.{}, .{}); return cmd.sendResult(.{}, .{});
} }
fn fillNode(cmd: *CDP.Command) !void { fn fillNode(cmd: anytype) !void {
const Params = struct { const Params = struct {
nodeId: ?Node.Id = null, nodeId: ?Node.Id = null,
backendNodeId: ?Node.Id = null, backendNodeId: ?Node.Id = null,
@@ -220,7 +216,7 @@ fn fillNode(cmd: *CDP.Command) !void {
return cmd.sendResult(.{}, .{}); return cmd.sendResult(.{}, .{});
} }
fn scrollNode(cmd: *CDP.Command) !void { fn scrollNode(cmd: anytype) !void {
const Params = struct { const Params = struct {
nodeId: ?Node.Id = null, nodeId: ?Node.Id = null,
backendNodeId: ?Node.Id = null, backendNodeId: ?Node.Id = null,
@@ -248,7 +244,7 @@ fn scrollNode(cmd: *CDP.Command) !void {
return cmd.sendResult(.{}, .{}); return cmd.sendResult(.{}, .{});
} }
fn waitForSelector(cmd: *CDP.Command) !void { fn waitForSelector(cmd: anytype) !void {
const Params = struct { const Params = struct {
selector: []const u8, selector: []const u8,
timeout: ?u32 = null, timeout: ?u32 = null,

View File

@@ -18,21 +18,18 @@
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda"); const lp = @import("lightpanda");
const Allocator = std.mem.Allocator;
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const id = @import("../id.zig"); const CdpStorage = @import("storage.zig");
const CDP = @import("../CDP.zig");
const id = @import("../id.zig");
const URL = @import("../../browser/URL.zig"); const URL = @import("../../browser/URL.zig");
const Transfer = @import("../../browser/HttpClient.zig").Transfer; const Transfer = @import("../../browser/HttpClient.zig").Transfer;
const Notification = @import("../../Notification.zig"); const Notification = @import("../../Notification.zig");
const Mime = @import("../../browser/Mime.zig"); const Mime = @import("../../browser/Mime.zig");
const CdpStorage = @import("storage.zig"); pub fn processMessage(cmd: anytype) !void {
const Allocator = std.mem.Allocator;
pub fn processMessage(cmd: *CDP.Command) !void {
const action = std.meta.stringToEnum(enum { const action = std.meta.stringToEnum(enum {
enable, enable,
disable, disable,
@@ -62,19 +59,19 @@ pub fn processMessage(cmd: *CDP.Command) !void {
} }
} }
fn enable(cmd: *CDP.Command) !void { fn enable(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
try bc.networkEnable(); try bc.networkEnable();
return cmd.sendResult(null, .{}); return cmd.sendResult(null, .{});
} }
fn disable(cmd: *CDP.Command) !void { fn disable(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
bc.networkDisable(); bc.networkDisable();
return cmd.sendResult(null, .{}); return cmd.sendResult(null, .{});
} }
fn setExtraHTTPHeaders(cmd: *CDP.Command) !void { fn setExtraHTTPHeaders(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
headers: std.json.ArrayHashMap([]const u8), headers: std.json.ArrayHashMap([]const u8),
})) orelse return error.InvalidParams; })) orelse return error.InvalidParams;
@@ -113,7 +110,7 @@ fn cookieMatches(cookie: *const Cookie, name: []const u8, domain: ?[]const u8, p
return true; return true;
} }
fn deleteCookies(cmd: *CDP.Command) !void { fn deleteCookies(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
name: []const u8, name: []const u8,
url: ?[:0]const u8 = null, url: ?[:0]const u8 = null,
@@ -147,14 +144,14 @@ fn deleteCookies(cmd: *CDP.Command) !void {
return cmd.sendResult(null, .{}); return cmd.sendResult(null, .{});
} }
fn clearBrowserCookies(cmd: *CDP.Command) !void { fn clearBrowserCookies(cmd: anytype) !void {
if (try cmd.params(struct {}) != null) return error.InvalidParams; if (try cmd.params(struct {}) != null) return error.InvalidParams;
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
bc.session.cookie_jar.clearRetainingCapacity(); bc.session.cookie_jar.clearRetainingCapacity();
return cmd.sendResult(null, .{}); return cmd.sendResult(null, .{});
} }
fn setCookie(cmd: *CDP.Command) !void { fn setCookie(cmd: anytype) !void {
const params = (try cmd.params( const params = (try cmd.params(
CdpStorage.CdpCookie, CdpStorage.CdpCookie,
)) orelse return error.InvalidParams; )) orelse return error.InvalidParams;
@@ -165,7 +162,7 @@ fn setCookie(cmd: *CDP.Command) !void {
try cmd.sendResult(.{ .success = true }, .{}); try cmd.sendResult(.{ .success = true }, .{});
} }
fn setCookies(cmd: *CDP.Command) !void { fn setCookies(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
cookies: []const CdpStorage.CdpCookie, cookies: []const CdpStorage.CdpCookie,
})) orelse return error.InvalidParams; })) orelse return error.InvalidParams;
@@ -181,7 +178,7 @@ fn setCookies(cmd: *CDP.Command) !void {
const GetCookiesParam = struct { const GetCookiesParam = struct {
urls: ?[]const [:0]const u8 = null, urls: ?[]const [:0]const u8 = null,
}; };
fn getCookies(cmd: *CDP.Command) !void { fn getCookies(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const params = (try cmd.params(GetCookiesParam)) orelse GetCookiesParam{}; const params = (try cmd.params(GetCookiesParam)) orelse GetCookiesParam{};
@@ -204,7 +201,7 @@ fn getCookies(cmd: *CDP.Command) !void {
try cmd.sendResult(.{ .cookies = writer }, .{}); try cmd.sendResult(.{ .cookies = writer }, .{});
} }
fn getResponseBody(cmd: *CDP.Command) !void { fn getResponseBody(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
requestId: []const u8, // "REQ-{d}" requestId: []const u8, // "REQ-{d}"
})) orelse return error.InvalidParams; })) orelse return error.InvalidParams;
@@ -230,7 +227,7 @@ fn getResponseBody(cmd: *CDP.Command) !void {
}, .{}); }, .{});
} }
pub fn httpRequestFail(bc: *CDP.BrowserContext, 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 // 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 // sent Target.closeTarget. In that case, bc.session_id will be cleared
// already, and we can skip sending these messages to the client. // already, and we can skip sending these messages to the client.
@@ -250,7 +247,7 @@ pub fn httpRequestFail(bc: *CDP.BrowserContext, msg: *const Notification.Request
}, .{ .session_id = session_id }); }, .{ .session_id = session_id });
} }
pub fn httpRequestStart(bc: *CDP.BrowserContext, 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 // detachTarget could be called, in which case, we still have a page doing
// things, but no session. // things, but no session.
const session_id = bc.session_id orelse return; const session_id = bc.session_id orelse return;
@@ -279,7 +276,7 @@ pub fn httpRequestStart(bc: *CDP.BrowserContext, msg: *const Notification.Reques
}, .{ .session_id = session_id }); }, .{ .session_id = session_id });
} }
pub fn httpResponseHeaderDone(arena: Allocator, bc: *CDP.BrowserContext, msg: *const Notification.ResponseHeaderDone) !void { pub fn httpResponseHeaderDone(arena: Allocator, bc: anytype, msg: *const Notification.ResponseHeaderDone) !void {
// detachTarget could be called, in which case, we still have a page doing // detachTarget could be called, in which case, we still have a page doing
// things, but no session. // things, but no session.
const session_id = bc.session_id orelse return; const session_id = bc.session_id orelse return;
@@ -296,7 +293,7 @@ pub fn httpResponseHeaderDone(arena: Allocator, bc: *CDP.BrowserContext, msg: *c
}, .{ .session_id = session_id }); }, .{ .session_id = session_id });
} }
pub fn httpRequestDone(bc: *CDP.BrowserContext, 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 // detachTarget could be called, in which case, we still have a page doing
// things, but no session. // things, but no session.
const session_id = bc.session_id orelse return; const session_id = bc.session_id orelse return;

View File

@@ -1,5 +1,4 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) // Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
// //
// Francis Bouvier <francis@lightpanda.io> // Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io> // Pierre Tachoire <pierre@lightpanda.io>
@@ -23,8 +22,6 @@ const lp = @import("lightpanda");
const screenshot_png = @embedFile("screenshot.png"); const screenshot_png = @embedFile("screenshot.png");
const id = @import("../id.zig"); const id = @import("../id.zig");
const CDP = @import("../CDP.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const js = @import("../../browser/js/js.zig"); const js = @import("../../browser/js/js.zig");
const URL = @import("../../browser/URL.zig"); const URL = @import("../../browser/URL.zig");
@@ -34,7 +31,7 @@ const Notification = @import("../../Notification.zig");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
pub fn processMessage(cmd: *CDP.Command) !void { pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum { const action = std.meta.stringToEnum(enum {
enable, enable,
getFrameTree, getFrameTree,
@@ -81,7 +78,7 @@ const Frame = struct {
gatedAPIFeatures: [][]const u8 = &[0][]const u8{}, gatedAPIFeatures: [][]const u8 = &[0][]const u8{},
}; };
fn getFrameTree(cmd: *CDP.Command) !void { fn getFrameTree(cmd: anytype) !void {
// Stagehand parses the response and error if we don't return a // 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. // correct one for this call when browser context or target id are missing.
const startup = .{ const startup = .{
@@ -111,7 +108,7 @@ fn getFrameTree(cmd: *CDP.Command) !void {
}, .{}); }, .{});
} }
fn setLifecycleEventsEnabled(cmd: *CDP.Command) !void { fn setLifecycleEventsEnabled(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
enabled: bool, enabled: bool,
})) orelse return error.InvalidParams; })) orelse return error.InvalidParams;
@@ -152,7 +149,7 @@ fn setLifecycleEventsEnabled(cmd: *CDP.Command) !void {
return cmd.sendResult(null, .{}); return cmd.sendResult(null, .{});
} }
fn addScriptToEvaluateOnNewDocument(cmd: *CDP.Command) !void { fn addScriptToEvaluateOnNewDocument(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
source: []const u8, source: []const u8,
worldName: ?[]const u8 = null, worldName: ?[]const u8 = null,
@@ -182,7 +179,7 @@ fn addScriptToEvaluateOnNewDocument(cmd: *CDP.Command) !void {
}, .{}); }, .{});
} }
fn removeScriptToEvaluateOnNewDocument(cmd: *CDP.Command) !void { fn removeScriptToEvaluateOnNewDocument(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
identifier: []const u8, identifier: []const u8,
})) orelse return error.InvalidParams; })) orelse return error.InvalidParams;
@@ -201,7 +198,7 @@ fn removeScriptToEvaluateOnNewDocument(cmd: *CDP.Command) !void {
return cmd.sendResult(null, .{}); return cmd.sendResult(null, .{});
} }
fn close(cmd: *CDP.Command) !void { fn close(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; 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;
@@ -238,7 +235,7 @@ fn close(cmd: *CDP.Command) !void {
bc.target_id = null; bc.target_id = null;
} }
fn createIsolatedWorld(cmd: *CDP.Command) !void { fn createIsolatedWorld(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
frameId: []const u8, frameId: []const u8,
worldName: []const u8, worldName: []const u8,
@@ -258,7 +255,7 @@ fn createIsolatedWorld(cmd: *CDP.Command) !void {
return cmd.sendResult(.{ .executionContextId = js_context.id }, .{}); return cmd.sendResult(.{ .executionContextId = js_context.id }, .{});
} }
fn navigate(cmd: *CDP.Command) !void { fn navigate(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
url: [:0]const u8, url: [:0]const u8,
// referrer: ?[]const u8 = null, // referrer: ?[]const u8 = null,
@@ -292,7 +289,7 @@ fn navigate(cmd: *CDP.Command) !void {
}); });
} }
fn doReload(cmd: *CDP.Command) !void { fn doReload(cmd: anytype) !void {
const params = try cmd.params(struct { const params = try cmd.params(struct {
ignoreCache: ?bool = null, ignoreCache: ?bool = null,
scriptToEvaluateOnLoad: ?[]const u8 = null, scriptToEvaluateOnLoad: ?[]const u8 = null,
@@ -322,7 +319,7 @@ fn doReload(cmd: *CDP.Command) !void {
}); });
} }
pub fn pageNavigate(bc: *CDP.BrowserContext, 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 // detachTarget could be called, in which case, we still have a page doing
// things, but no session. // things, but no session.
const session_id = bc.session_id orelse return; const session_id = bc.session_id orelse return;
@@ -374,7 +371,7 @@ pub fn pageNavigate(bc: *CDP.BrowserContext, event: *const Notification.PageNavi
}, .{ .session_id = session_id }); }, .{ .session_id = session_id });
} }
pub fn pageRemove(bc: *CDP.BrowserContext) !void { pub fn pageRemove(bc: anytype) !void {
// Clear all remote object mappings to prevent stale objectIds from being used // Clear all remote object mappings to prevent stale objectIds from being used
// after the context is destroy // after the context is destroy
bc.inspector_session.inspector.resetContextGroup(); bc.inspector_session.inspector.resetContextGroup();
@@ -385,7 +382,7 @@ pub fn pageRemove(bc: *CDP.BrowserContext) !void {
} }
} }
pub fn pageCreated(bc: *CDP.BrowserContext, page: *Page) !void { pub fn pageCreated(bc: anytype, page: *Page) !void {
_ = bc.cdp.page_arena.reset(.{ .retain_with_limit = 1024 * 512 }); _ = bc.cdp.page_arena.reset(.{ .retain_with_limit = 1024 * 512 });
for (bc.isolated_worlds.items) |isolated_world| { for (bc.isolated_worlds.items) |isolated_world| {
@@ -397,7 +394,7 @@ pub fn pageCreated(bc: *CDP.BrowserContext, page: *Page) !void {
bc.captured_responses = .empty; bc.captured_responses = .empty;
} }
pub fn pageFrameCreated(bc: *CDP.BrowserContext, event: *const Notification.PageFrameCreated) !void { pub fn pageFrameCreated(bc: anytype, event: *const Notification.PageFrameCreated) !void {
const session_id = bc.session_id orelse return; const session_id = bc.session_id orelse return;
const cdp = bc.cdp; const cdp = bc.cdp;
@@ -418,7 +415,7 @@ pub fn pageFrameCreated(bc: *CDP.BrowserContext, event: *const Notification.Page
} }
} }
pub fn pageNavigated(arena: Allocator, bc: *CDP.BrowserContext, event: *const Notification.PageNavigated) !void { pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.PageNavigated) !void {
// detachTarget could be called, in which case, we still have a page doing // detachTarget could be called, in which case, we still have a page doing
// things, but no session. // things, but no session.
const session_id = bc.session_id orelse return; const session_id = bc.session_id orelse return;
@@ -600,15 +597,15 @@ pub fn pageNavigated(arena: Allocator, bc: *CDP.BrowserContext, event: *const No
}, .{ .session_id = session_id }); }, .{ .session_id = session_id });
} }
pub fn pageNetworkIdle(bc: *CDP.BrowserContext, event: *const Notification.PageNetworkIdle) !void { pub fn pageNetworkIdle(bc: anytype, event: *const Notification.PageNetworkIdle) !void {
return sendPageLifecycle(bc, "networkIdle", event.timestamp, &id.toFrameId(event.frame_id), &id.toLoaderId(event.req_id)); return sendPageLifecycle(bc, "networkIdle", event.timestamp, &id.toFrameId(event.frame_id), &id.toLoaderId(event.req_id));
} }
pub fn pageNetworkAlmostIdle(bc: *CDP.BrowserContext, event: *const Notification.PageNetworkAlmostIdle) !void { pub fn pageNetworkAlmostIdle(bc: anytype, event: *const Notification.PageNetworkAlmostIdle) !void {
return sendPageLifecycle(bc, "networkAlmostIdle", event.timestamp, &id.toFrameId(event.frame_id), &id.toLoaderId(event.req_id)); return sendPageLifecycle(bc, "networkAlmostIdle", event.timestamp, &id.toFrameId(event.frame_id), &id.toLoaderId(event.req_id));
} }
fn sendPageLifecycle(bc: *CDP.BrowserContext, name: []const u8, timestamp: u64, frame_id: []const u8, loader_id: []const u8) !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 // detachTarget could be called, in which case, we still have a page doing
// things, but no session. // things, but no session.
const session_id = bc.session_id orelse return; const session_id = bc.session_id orelse return;
@@ -643,7 +640,7 @@ fn base64Encode(comptime input: []const u8) [std.base64.standard.Encoder.calcSiz
return buf; return buf;
} }
fn captureScreenshot(cmd: *CDP.Command) !void { fn captureScreenshot(cmd: anytype) !void {
const Params = struct { const Params = struct {
format: ?[]const u8 = "png", format: ?[]const u8 = "png",
quality: ?u8 = null, quality: ?u8 = null,
@@ -679,7 +676,7 @@ fn captureScreenshot(cmd: *CDP.Command) !void {
}, .{}); }, .{});
} }
fn getLayoutMetrics(cmd: *CDP.Command) !void { fn getLayoutMetrics(cmd: anytype) !void {
const width = 1920; const width = 1920;
const height = 1080; const height = 1080;

View File

@@ -17,9 +17,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const CDP = @import("../CDP.zig");
pub fn processMessage(cmd: *CDP.Command) !void { pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum { const action = std.meta.stringToEnum(enum {
enable, enable,
disable, disable,

View File

@@ -19,9 +19,7 @@
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const CDP = @import("../CDP.zig"); pub fn processMessage(cmd: anytype) !void {
pub fn processMessage(cmd: *CDP.Command) !void {
const action = std.meta.stringToEnum(enum { const action = std.meta.stringToEnum(enum {
enable, enable,
runIfWaitingForDebugger, runIfWaitingForDebugger,
@@ -38,7 +36,7 @@ pub fn processMessage(cmd: *CDP.Command) !void {
} }
} }
fn sendInspector(cmd: *CDP.Command, action: anytype) !void { fn sendInspector(cmd: anytype, action: anytype) !void {
// save script in file at debug mode // save script in file at debug mode
if (builtin.mode == .Debug) { if (builtin.mode == .Debug) {
try logInspector(cmd, action); try logInspector(cmd, action);
@@ -50,7 +48,7 @@ fn sendInspector(cmd: *CDP.Command, action: anytype) !void {
bc.callInspector(cmd.input.json); bc.callInspector(cmd.input.json);
} }
fn logInspector(cmd: *CDP.Command, action: anytype) !void { fn logInspector(cmd: anytype, action: anytype) !void {
const script = switch (action) { const script = switch (action) {
.evaluate => blk: { .evaluate => blk: {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {

View File

@@ -17,9 +17,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const CDP = @import("../CDP.zig");
pub fn processMessage(cmd: *CDP.Command) !void { pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum { const action = std.meta.stringToEnum(enum {
enable, enable,
disable, disable,
@@ -33,7 +32,7 @@ pub fn processMessage(cmd: *CDP.Command) !void {
} }
} }
fn setIgnoreCertificateErrors(cmd: *CDP.Command) !void { fn setIgnoreCertificateErrors(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
ignore: bool, ignore: bool,
})) orelse return error.InvalidParams; })) orelse return error.InvalidParams;

View File

@@ -18,16 +18,13 @@
const std = @import("std"); const std = @import("std");
const CDP = @import("../CDP.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const URL = @import("../../browser/URL.zig"); const URL = @import("../../browser/URL.zig");
const Cookie = @import("../../browser/webapi/storage/storage.zig").Cookie; const Cookie = @import("../../browser/webapi/storage/storage.zig").Cookie;
const CookieJar = Cookie.Jar; const CookieJar = Cookie.Jar;
pub const PreparedUri = Cookie.PreparedUri; pub const PreparedUri = Cookie.PreparedUri;
pub fn processMessage(cmd: *CDP.Command) !void { pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum { const action = std.meta.stringToEnum(enum {
clearCookies, clearCookies,
setCookies, setCookies,
@@ -43,7 +40,7 @@ pub fn processMessage(cmd: *CDP.Command) !void {
const BrowserContextParam = struct { browserContextId: ?[]const u8 = null }; const BrowserContextParam = struct { browserContextId: ?[]const u8 = null };
fn clearCookies(cmd: *CDP.Command) !void { fn clearCookies(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const params = (try cmd.params(BrowserContextParam)) orelse BrowserContextParam{}; const params = (try cmd.params(BrowserContextParam)) orelse BrowserContextParam{};
@@ -58,7 +55,7 @@ fn clearCookies(cmd: *CDP.Command) !void {
return cmd.sendResult(null, .{}); return cmd.sendResult(null, .{});
} }
fn getCookies(cmd: *CDP.Command) !void { fn getCookies(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const params = (try cmd.params(BrowserContextParam)) orelse BrowserContextParam{}; const params = (try cmd.params(BrowserContextParam)) orelse BrowserContextParam{};
@@ -72,7 +69,7 @@ fn getCookies(cmd: *CDP.Command) !void {
try cmd.sendResult(.{ .cookies = writer }, .{}); try cmd.sendResult(.{ .cookies = writer }, .{});
} }
fn setCookies(cmd: *CDP.Command) !void { fn setCookies(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
cookies: []const CdpCookie, cookies: []const CdpCookie,

View File

@@ -20,13 +20,11 @@ const std = @import("std");
const lp = @import("lightpanda"); const lp = @import("lightpanda");
const id = @import("../id.zig"); const id = @import("../id.zig");
const CDP = @import("../CDP.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const URL = @import("../../browser/URL.zig"); const URL = @import("../../browser/URL.zig");
const js = @import("../../browser/js/js.zig"); const js = @import("../../browser/js/js.zig");
pub fn processMessage(cmd: *CDP.Command) !void { pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum { const action = std.meta.stringToEnum(enum {
getTargets, getTargets,
attachToTarget, attachToTarget,
@@ -62,7 +60,7 @@ pub fn processMessage(cmd: *CDP.Command) !void {
} }
} }
fn getTargets(cmd: *CDP.Command) !void { fn getTargets(cmd: anytype) !void {
// If no context available, return an empty array. // If no context available, return an empty array.
const bc = cmd.browser_context orelse { const bc = cmd.browser_context orelse {
return cmd.sendResult(.{ return cmd.sendResult(.{
@@ -88,7 +86,7 @@ fn getTargets(cmd: *CDP.Command) !void {
}, .{ .include_session_id = false }); }, .{ .include_session_id = false });
} }
fn getBrowserContexts(cmd: *CDP.Command) !void { fn getBrowserContexts(cmd: anytype) !void {
var browser_context_ids: []const []const u8 = undefined; var browser_context_ids: []const []const u8 = undefined;
if (cmd.browser_context) |bc| { if (cmd.browser_context) |bc| {
browser_context_ids = &.{bc.id}; browser_context_ids = &.{bc.id};
@@ -101,7 +99,7 @@ fn getBrowserContexts(cmd: *CDP.Command) !void {
}, .{ .include_session_id = false }); }, .{ .include_session_id = false });
} }
fn createBrowserContext(cmd: *CDP.Command) !void { fn createBrowserContext(cmd: anytype) !void {
const params = try cmd.params(struct { const params = try cmd.params(struct {
disposeOnDetach: bool = false, disposeOnDetach: bool = false,
proxyServer: ?[:0]const u8 = null, proxyServer: ?[:0]const u8 = null,
@@ -132,7 +130,7 @@ fn createBrowserContext(cmd: *CDP.Command) !void {
}, .{}); }, .{});
} }
fn disposeBrowserContext(cmd: *CDP.Command) !void { fn disposeBrowserContext(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
browserContextId: []const u8, browserContextId: []const u8,
})) orelse return error.InvalidParams; })) orelse return error.InvalidParams;
@@ -143,7 +141,7 @@ fn disposeBrowserContext(cmd: *CDP.Command) !void {
try cmd.sendResult(null, .{}); try cmd.sendResult(null, .{});
} }
fn createTarget(cmd: *CDP.Command) !void { fn createTarget(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
url: [:0]const u8 = "about:blank", url: [:0]const u8 = "about:blank",
// width: ?u64 = null, // width: ?u64 = null,
@@ -160,15 +158,20 @@ fn createTarget(cmd: *CDP.Command) !void {
else => return err, else => return err,
}; };
if (bc.target_id != null) {
return error.TargetAlreadyLoaded;
}
if (params.browserContextId) |param_browser_context_id| { if (params.browserContextId) |param_browser_context_id| {
if (std.mem.eql(u8, param_browser_context_id, bc.id) == false) { if (std.mem.eql(u8, param_browser_context_id, bc.id) == false) {
return error.UnknownBrowserContextId; return error.UnknownBrowserContextId;
} }
} }
// If a target already exists, close it first. Lightpanda only supports
// one page at a time, so we replace the existing target rather than
// rejecting the request. This unblocks automation frameworks (e.g.
// Stagehand) that call createTarget multiple times.
if (bc.target_id != null) {
try doCloseTarget(cmd, bc);
}
// if target_id is null, we should never have a page // if target_id is null, we should never have a page
lp.assert(bc.session.page == null, "CDP.target.createTarget not null page", .{}); lp.assert(bc.session.page == null, "CDP.target.createTarget not null page", .{});
@@ -232,7 +235,7 @@ fn createTarget(cmd: *CDP.Command) !void {
}, .{}); }, .{});
} }
fn attachToTarget(cmd: *CDP.Command) !void { fn attachToTarget(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
targetId: []const u8, targetId: []const u8,
flatten: bool = true, flatten: bool = true,
@@ -249,7 +252,7 @@ fn attachToTarget(cmd: *CDP.Command) !void {
return cmd.sendResult(.{ .sessionId = bc.session_id }, .{}); return cmd.sendResult(.{ .sessionId = bc.session_id }, .{});
} }
fn attachToBrowserTarget(cmd: *CDP.Command) !void { fn attachToBrowserTarget(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const session_id = bc.session_id orelse cmd.cdp.session_id_gen.next(); const session_id = bc.session_id orelse cmd.cdp.session_id_gen.next();
@@ -271,7 +274,7 @@ fn attachToBrowserTarget(cmd: *CDP.Command) !void {
return cmd.sendResult(.{ .sessionId = bc.session_id }, .{}); return cmd.sendResult(.{ .sessionId = bc.session_id }, .{});
} }
fn closeTarget(cmd: *CDP.Command) !void { fn closeTarget(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
targetId: []const u8, targetId: []const u8,
})) orelse return error.InvalidParams; })) orelse return error.InvalidParams;
@@ -282,37 +285,12 @@ fn closeTarget(cmd: *CDP.Command) !void {
return error.UnknownTargetId; return error.UnknownTargetId;
} }
// can't be null if we have a target_id
lp.assert(bc.session.page != null, "CDP.target.closeTarget null page", .{});
try cmd.sendResult(.{ .success = true }, .{ .include_session_id = false }); try cmd.sendResult(.{ .success = true }, .{ .include_session_id = false });
// could be null, created but never attached try doCloseTarget(cmd, bc);
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 getTargetInfo(cmd: *CDP.Command) !void { fn getTargetInfo(cmd: anytype) !void {
const Params = struct { const Params = struct {
targetId: ?[]const u8 = null, targetId: ?[]const u8 = null,
}; };
@@ -349,7 +327,7 @@ fn getTargetInfo(cmd: *CDP.Command) !void {
}, .{ .include_session_id = false }); }, .{ .include_session_id = false });
} }
fn sendMessageToTarget(cmd: *CDP.Command) !void { fn sendMessageToTarget(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
message: []const u8, message: []const u8,
sessionId: []const u8, sessionId: []const u8,
@@ -367,19 +345,32 @@ fn sendMessageToTarget(cmd: *CDP.Command) !void {
return error.UnknownSessionId; return error.UnknownSessionId;
} }
var aw = std.Io.Writer.Allocating.init(cmd.arena); const Capture = struct {
cmd.cdp.dispatch(cmd.arena, .{ .capture = &aw.writer }, params.message) catch |err| { 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| {
log.err(.cdp, "internal dispatch error", .{ .err = err, .id = cmd.input.id, .message = params.message }); log.err(.cdp, "internal dispatch error", .{ .err = err, .id = cmd.input.id, .message = params.message });
return err; return err;
}; };
try cmd.sendEvent("Target.receivedMessageFromTarget", .{ try cmd.sendEvent("Target.receivedMessageFromTarget", .{
.message = aw.written(), .message = capture.aw.written(),
.sessionId = params.sessionId, .sessionId = params.sessionId,
}, .{}); }, .{});
} }
fn detachFromTarget(cmd: *CDP.Command) !void { fn detachFromTarget(cmd: anytype) !void {
if (cmd.browser_context) |bc| { if (cmd.browser_context) |bc| {
if (bc.session_id) |session_id| { if (bc.session_id) |session_id| {
try cmd.sendEvent("Target.detachedFromTarget", .{ try cmd.sendEvent("Target.detachedFromTarget", .{
@@ -393,11 +384,11 @@ fn detachFromTarget(cmd: *CDP.Command) !void {
} }
// TODO: noop method // TODO: noop method
fn setDiscoverTargets(cmd: *CDP.Command) !void { fn setDiscoverTargets(cmd: anytype) !void {
return cmd.sendResult(null, .{}); return cmd.sendResult(null, .{});
} }
fn setAutoAttach(cmd: *CDP.Command) !void { fn setAutoAttach(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
autoAttach: bool, autoAttach: bool,
waitForDebuggerOnStart: bool, waitForDebuggerOnStart: bool,
@@ -457,7 +448,42 @@ fn setAutoAttach(cmd: *CDP.Command) !void {
try cmd.sendResult(null, .{}); try cmd.sendResult(null, .{});
} }
fn doAttachtoTarget(cmd: *CDP.Command, target_id: []const u8) !void { /// Close the current target in a browser context: send detach events,
/// remove the page, clean up isolated worlds, and clear the target_id.
/// Shared by closeTarget and createTarget (which auto-closes before
/// creating a replacement).
fn doCloseTarget(cmd: anytype, bc: anytype) !void {
// can't be null if we have a target_id
lp.assert(bc.session.page != null, "CDP.target.doCloseTarget null page", .{});
// could be null, created but never attached
if (bc.session_id) |session_id| {
try cmd.sendEvent("Inspector.detached", .{
.reason = "Render process gone.",
}, .{ .session_id = session_id });
try cmd.sendEvent("Target.detachedFromTarget", .{
.targetId = &bc.target_id.?,
.sessionId = session_id,
.reason = "Render process gone.",
}, .{});
bc.session_id = null;
}
try cmd.sendEvent("Target.targetDestroyed", .{
.targetId = &bc.target_id.?,
}, .{});
bc.session.removePage();
for (bc.isolated_worlds.items) |world| {
world.deinit();
}
bc.isolated_worlds.clearRetainingCapacity();
bc.target_id = null;
}
fn doAttachtoTarget(cmd: anytype, target_id: []const u8) !void {
const bc = cmd.browser_context.?; const bc = cmd.browser_context.?;
const session_id = bc.session_id orelse cmd.cdp.session_id_gen.next(); const session_id = bc.session_id orelse cmd.cdp.session_id_gen.next();
@@ -635,6 +661,7 @@ test "cdp.target: closeTarget" {
{ {
try ctx.processMessage(.{ .id = 11, .method = "Target.closeTarget", .params = .{ .targetId = "TID-000000000A" } }); try ctx.processMessage(.{ .id = 11, .method = "Target.closeTarget", .params = .{ .targetId = "TID-000000000A" } });
try ctx.expectSentResult(.{ .success = true }, .{ .id = 11 }); try ctx.expectSentResult(.{ .success = true }, .{ .id = 11 });
try ctx.expectSentEvent("Target.targetDestroyed", .{ .targetId = "TID-000000000A" }, .{});
try testing.expectEqual(null, bc.session.page); try testing.expectEqual(null, bc.session.page);
try testing.expectEqual(null, bc.target_id); try testing.expectEqual(null, bc.target_id);
} }
@@ -760,6 +787,53 @@ test "cdp.target: detachFromTarget" {
} }
} }
test "cdp.target: createTarget closes existing target (issue #1962)" {
var ctx = try testing.context();
defer ctx.deinit();
const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" });
{
// Create first target
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .browserContextId = "BID-9" } });
try testing.expectEqual(true, bc.target_id != null);
try ctx.expectSentResult(.{ .targetId = bc.target_id.? }, .{ .id = 10 });
// Create second target — should succeed by auto-closing the first
try ctx.processMessage(.{ .id = 11, .method = "Target.createTarget", .params = .{ .browserContextId = "BID-9" } });
try ctx.expectSentEvent("Target.targetDestroyed", .{ .targetId = "FID-0000000001" }, .{});
try testing.expectEqual(true, bc.target_id != null);
try ctx.expectSentResult(.{ .targetId = bc.target_id.? }, .{ .id = 11 });
// Page should exist (new target is active)
try testing.expectEqual(true, bc.session.page != null);
}
}
test "cdp.target: createTarget closes existing attached target (issue #1962)" {
var ctx = try testing.context();
defer ctx.deinit();
const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" });
{
// Create and attach first target
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .browserContextId = "BID-9" } });
try testing.expectEqual(true, bc.target_id != null);
try ctx.expectSentResult(.{ .targetId = bc.target_id.? }, .{ .id = 10 });
try ctx.processMessage(.{ .id = 11, .method = "Target.attachToTarget", .params = .{ .targetId = bc.target_id.? } });
const session_id = bc.session_id.?;
try ctx.expectSentResult(.{ .sessionId = session_id }, .{ .id = 11 });
// Create second target — should close and detach the first
try ctx.processMessage(.{ .id = 12, .method = "Target.createTarget", .params = .{ .browserContextId = "BID-9" } });
// Should have sent detach events for the old session
try ctx.expectSentEvent("Inspector.detached", .{ .reason = "Render process gone." }, .{ .session_id = session_id });
try ctx.expectSentEvent("Target.detachedFromTarget", .{ .sessionId = session_id, .reason = "Render process gone." }, .{});
try ctx.expectSentEvent("Target.targetDestroyed", .{ .targetId = "FID-0000000001" }, .{});
// New target should be created
try testing.expectEqual(true, bc.target_id != null);
try ctx.expectSentResult(.{ .targetId = bc.target_id.? }, .{ .id = 12 });
}
}
test "cdp.target: detachFromTarget without session" { test "cdp.target: detachFromTarget without session" {
var ctx = try testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();

View File

@@ -64,7 +64,7 @@ const TestContext = struct {
session_id: ?[]const u8 = null, session_id: ?[]const u8 = null,
url: ?[:0]const u8 = null, url: ?[:0]const u8 = null,
}; };
pub fn loadBrowserContext(self: *TestContext, opts: BrowserContextOpts) !*CDP.BrowserContext { pub fn loadBrowserContext(self: *TestContext, opts: BrowserContextOpts) !*CDP.BrowserContext(CDP) {
var c = self.cdp(); var c = self.cdp();
if (c.browser_context) |bc| { if (c.browser_context) |bc| {
_ = c.disposeBrowserContext(bc.id); _ = c.disposeBrowserContext(bc.id);

View File

@@ -1,6 +1,7 @@
const std = @import("std"); const std = @import("std");
pub const protocol = @import("mcp/protocol.zig"); pub const protocol = @import("mcp/protocol.zig");
pub const Version = protocol.Version;
pub const router = @import("mcp/router.zig"); pub const router = @import("mcp/router.zig");
pub const Server = @import("mcp/Server.zig"); pub const Server = @import("mcp/Server.zig");

View File

@@ -114,7 +114,7 @@ test "MCP.Server - Integration: synchronous smoke test" {
try router.processRequests(server, &in_reader); try router.processRequests(server, &in_reader);
try testing.expectJson(.{ .jsonrpc = "2.0", .id = 1 }, out_alloc.writer.buffered()); try testing.expectJson(.{ .jsonrpc = "2.0", .id = 1, .result = .{ .protocolVersion = "2024-11-05" } }, out_alloc.writer.buffered());
} }
test "MCP.Server - Integration: ping request returns an empty result" { test "MCP.Server - Integration: ping request returns an empty result" {

View File

@@ -1,5 +1,14 @@
const std = @import("std"); const std = @import("std");
pub const Version = enum {
@"2024-11-05",
@"2025-03-26",
@"2025-06-18",
@"2025-11-25",
pub const default: Version = .@"2024-11-05";
};
pub const Request = struct { pub const Request = struct {
jsonrpc: []const u8 = "2.0", jsonrpc: []const u8 = "2.0",
id: ?std.json.Value = null, id: ?std.json.Value = null,

View File

@@ -81,8 +81,12 @@ pub fn handleMessage(server: *Server, arena: std.mem.Allocator, msg: []const u8)
fn handleInitialize(server: *Server, req: protocol.Request) !void { fn handleInitialize(server: *Server, req: protocol.Request) !void {
const id = req.id orelse return; const id = req.id orelse return;
const result = protocol.InitializeResult{ const version: protocol.Version = switch (server.app.config.mode) {
.protocolVersion = "2025-11-25", .mcp => |opts| opts.version,
else => .default,
};
const result: protocol.InitializeResult = .{
.protocolVersion = @tagName(version),
.capabilities = .{ .capabilities = .{
.resources = .{}, .resources = .{},
.tools = .{}, .tools = .{},
@@ -121,7 +125,7 @@ test "MCP.router - handleMessage - synchronous unit tests" {
\\{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test-client","version":"1.0.0"}}} \\{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test-client","version":"1.0.0"}}}
); );
try testing.expectJson( try testing.expectJson(
\\{ "jsonrpc": "2.0", "id": 1, "result": { "capabilities": { "tools": {} } } } \\{ "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2024-11-05", "capabilities": { "tools": {} } } }
, out_alloc.writer.buffered()); , out_alloc.writer.buffered());
out_alloc.writer.end = 0; out_alloc.writer.end = 0;