mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-10-08 20:32:00 -04:00
Merge pull request #3571 from lightpanda-io/nikneym/playwright-id-track
`cdp`: answer inspector commands on the session that sent them
This commit is contained in:
10 files changed
+383
-177
No files matched your search
@@ -17,7 +17,6 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
const js = @import("js.zig");
|
||||
const v8 = js.v8;
|
||||
|
||||
@@ -42,7 +41,6 @@ isolate: *v8.Isolate,
|
||||
handle: *v8.Inspector,
|
||||
client: *v8.InspectorClientImpl,
|
||||
default_context: ?v8.Global,
|
||||
session: ?Session,
|
||||
|
||||
pub fn init(allocator: Allocator, isolate: *v8.Isolate) !*Inspector {
|
||||
const self = try allocator.create(Inspector);
|
||||
@@ -50,7 +48,6 @@ pub fn init(allocator: Allocator, isolate: *v8.Isolate) !*Inspector {
|
||||
|
||||
self.* = .{
|
||||
.unique_id = 1,
|
||||
.session = null,
|
||||
.isolate = isolate,
|
||||
.client = undefined,
|
||||
.handle = undefined,
|
||||
@@ -72,29 +69,11 @@ pub fn deinit(self: *const Inspector, allocator: Allocator) void {
|
||||
v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate);
|
||||
defer v8.v8__HandleScope__DESTRUCT(&hs);
|
||||
|
||||
if (self.session) |*s| {
|
||||
s.deinit();
|
||||
}
|
||||
v8.v8_inspector__Client__IMPL__DELETE(self.client);
|
||||
v8.v8_inspector__Inspector__DELETE(self.handle);
|
||||
allocator.destroy(self);
|
||||
}
|
||||
|
||||
pub fn startSession(self: *Inspector, ctx: anytype) *Session {
|
||||
if (comptime lp.IS_DEBUG) {
|
||||
std.debug.assert(self.session == null);
|
||||
}
|
||||
|
||||
self.session = @as(Session, undefined);
|
||||
Session.init(&self.session.?, self, ctx);
|
||||
return &self.session.?;
|
||||
}
|
||||
|
||||
pub fn stopSession(self: *Inspector) void {
|
||||
self.session.?.deinit();
|
||||
self.session = null;
|
||||
}
|
||||
|
||||
// From CDP docs
|
||||
// https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#type-ExecutionContextDescription
|
||||
// ----
|
||||
@@ -199,7 +178,10 @@ const RemoteObject = struct {
|
||||
// Combines a v8::InspectorSession and a v8::InspectorChannelImpl. The
|
||||
// InspectorSession is for zig -> v8 (sending messages to the inspector). The
|
||||
// Channel is for v8 -> zig, getting events from the Inspector (that we'll pass
|
||||
// back to some opaque context, i.e the CDP BrowserContext).
|
||||
// back to some opaque context, i.e the CDP AttachedSession).
|
||||
// The channel keeps the Session's address, so the owner must not move it
|
||||
// between init and deinit. Every Session connects to the same
|
||||
// CONTEXT_GROUP_ID and so sees every context.
|
||||
// The channel callbacks are defined below, as:
|
||||
// pub export fn v8_inspector__Channel__IMPL__XYZ
|
||||
pub const Session = struct {
|
||||
@@ -212,7 +194,7 @@ pub const Session = struct {
|
||||
onNotif: *const fn (ctx: *anyopaque, msg: []const u8) void,
|
||||
onResp: *const fn (ctx: *anyopaque, call_id: u32, msg: []const u8) void,
|
||||
|
||||
fn init(self: *Session, inspector: *Inspector, ctx: anytype) void {
|
||||
pub fn init(self: *Session, inspector: *Inspector, ctx: anytype) void {
|
||||
const Container = @typeInfo(@TypeOf(ctx)).pointer.child;
|
||||
|
||||
const channel = v8.v8_inspector__Channel__IMPL__CREATE(inspector.isolate);
|
||||
@@ -234,11 +216,23 @@ pub const Session = struct {
|
||||
};
|
||||
}
|
||||
|
||||
fn deinit(self: *const Session) void {
|
||||
pub fn deinit(self: *Session) void {
|
||||
// Deleting the V8 session fails its pending evaluations, which V8
|
||||
// answers through the channel. The client is gone: drop them.
|
||||
self.onResp = dropResponse;
|
||||
self.onNotif = dropNotification;
|
||||
|
||||
var hs: v8.HandleScope = undefined;
|
||||
v8.v8__HandleScope__CONSTRUCT(&hs, self.inspector.isolate);
|
||||
defer v8.v8__HandleScope__DESTRUCT(&hs);
|
||||
|
||||
v8.v8_inspector__Session__DELETE(self.handle);
|
||||
v8.v8_inspector__Channel__IMPL__DELETE(self.channel);
|
||||
}
|
||||
|
||||
fn dropResponse(_: *anyopaque, _: u32, _: []const u8) void {}
|
||||
fn dropNotification(_: *anyopaque, _: []const u8) void {}
|
||||
|
||||
pub fn send(self: *const Session, msg: []const u8) void {
|
||||
const isolate = self.inspector.isolate;
|
||||
var hs: v8.HandleScope = undefined;
|
||||
|
||||
+124
-78
@@ -333,18 +333,8 @@ pub fn resolveSessionId(self: *const CDP, input_session_id: []const u8) ?[]const
|
||||
return browser_session_id;
|
||||
}
|
||||
}
|
||||
const browser_context = &(self.browser_context orelse return null);
|
||||
if (browser_context.session_id) |session_id| {
|
||||
if (std.mem.eql(u8, session_id, input_session_id)) {
|
||||
return session_id;
|
||||
}
|
||||
}
|
||||
for (browser_context.attached_sessions.items) |session| {
|
||||
if (std.mem.eql(u8, session.id, input_session_id)) {
|
||||
return session.id;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
const browser_context = if (self.browser_context) |*bc| bc else return null;
|
||||
return browser_context.attached_sessions.getKey(input_session_id);
|
||||
}
|
||||
|
||||
fn isValidSessionId(self: *const CDP, input_session_id: []const u8) bool {
|
||||
@@ -407,9 +397,41 @@ pub const BrowserContext = struct {
|
||||
id: u32,
|
||||
};
|
||||
|
||||
const AttachedSession = struct {
|
||||
// A CDP session attached to this context's page target. Each owns a V8
|
||||
// inspector session; V8 keeps `Runtime.enable` state and remote object ids
|
||||
// per session, so a command is answered on the session it was sent
|
||||
// through and a session only gets the inspector events it enabled.
|
||||
pub const AttachedSession = struct {
|
||||
id: []const u8,
|
||||
parent_id: ?[]const u8,
|
||||
bc: *BrowserContext,
|
||||
inspector_session: js.Inspector.Session,
|
||||
|
||||
/// V8 -> this session. Stamp OUR id on responses and events.
|
||||
pub fn onInspectorResponse(ctx: *anyopaque, _: u32, msg: []const u8) void {
|
||||
const self: *AttachedSession = @ptrCast(@alignCast(ctx));
|
||||
self.bc.sendInspectorMessage(msg, self.id) catch |err| {
|
||||
log.err(.cdp, "send inspector response", .{ .err = err });
|
||||
};
|
||||
}
|
||||
|
||||
pub fn onInspectorEvent(ctx: *anyopaque, msg: []const u8) void {
|
||||
const self: *AttachedSession = @ptrCast(@alignCast(ctx));
|
||||
if (log.enabled(.cdp, .debug)) {
|
||||
// msg should be {"method":<method>,...
|
||||
lp.assert(std.mem.startsWith(u8, msg, "{\"method\":"), "onInspectorEvent prefix", .{});
|
||||
const method_end = std.mem.indexOfScalar(u8, msg, ',') orelse {
|
||||
log.err(.cdp, "invalid inspector event", .{ .msg = msg });
|
||||
return;
|
||||
};
|
||||
const method = msg[10..method_end];
|
||||
log.debug(.cdp, "inspector event", .{ .method = method, .session_id = self.id });
|
||||
}
|
||||
|
||||
self.bc.sendInspectorMessage(msg, self.id) catch |err| {
|
||||
log.err(.cdp, "send inspector event", .{ .err = err });
|
||||
};
|
||||
}
|
||||
};
|
||||
id: []const u8,
|
||||
cdp: *CDP,
|
||||
@@ -444,16 +466,18 @@ pub const BrowserContext = struct {
|
||||
// it). null until the first page is created.
|
||||
page_handle: ?Session.PageHandle = null,
|
||||
|
||||
// The CDP session_id. After the target/page is created, the client
|
||||
// "attaches" to it (either explicitly or automatically). We return a
|
||||
// "sessionId" which identifies this link. `sessionId` is the how
|
||||
// the CDP client informs us what it's trying to manipulate. Because we
|
||||
// only support 1 BrowserContext at a time, and 1 page at a time, this
|
||||
// is all pretty straightforward, but it still needs to be enforced, i.e.
|
||||
// if we get a request with a sessionId that doesn't match the current one
|
||||
// we should reject it.
|
||||
// The primary CDP session's id. After the target/page is created, the
|
||||
// client "attaches" to it (either explicitly or automatically). We return
|
||||
// a "sessionId" which identifies this link. `sessionId` is the how the
|
||||
// CDP client informs us what it's trying to manipulate. Also included in
|
||||
// `attached_sessions`.
|
||||
session_id: ?[]const u8,
|
||||
attached_sessions: std.ArrayList(AttachedSession) = .empty,
|
||||
|
||||
// Every session attached to the page target, the primary included, by
|
||||
// session id. Insertion-ordered so detach events come out in attach order.
|
||||
//
|
||||
// Request with a session id that isn't there is rejected.
|
||||
attached_sessions: std.StringArrayHashMapUnmanaged(*AttachedSession) = .empty,
|
||||
|
||||
// A cancelled text-less keyDown drops the char message that follows it
|
||||
// (chromedp's keyDown/char/keyUp split), as Chrome does.
|
||||
@@ -470,7 +494,6 @@ pub const BrowserContext = struct {
|
||||
// entries evicted by resetFrame can linger harmlessly until reset.
|
||||
set_child_nodes_sent: std.AutoHashMapUnmanaged(NodeRegistry.Id, void) = .empty,
|
||||
|
||||
inspector_session: *js.Inspector.Session,
|
||||
isolated_worlds: std.ArrayList(*IsolatedWorld),
|
||||
|
||||
// True when Runtime.evaluate has been run in the main world. Optimization
|
||||
@@ -533,10 +556,6 @@ pub const BrowserContext = struct {
|
||||
lp.cookies.loadFromFile(session, cookie_path);
|
||||
}
|
||||
|
||||
const browser = &cdp.browser;
|
||||
const inspector_session = browser.env.inspector.?.startSession(self);
|
||||
errdefer browser.env.inspector.?.stopSession();
|
||||
|
||||
var registry = NodeRegistry.init(allocator);
|
||||
errdefer registry.deinit();
|
||||
|
||||
@@ -552,7 +571,6 @@ pub const BrowserContext = struct {
|
||||
.node_registry = registry,
|
||||
.node_search_list = undefined,
|
||||
.isolated_worlds = .empty,
|
||||
.inspector_session = inspector_session,
|
||||
.frame_arena = cdp.frame_arena.allocator(),
|
||||
.arena = cdp.browser_context_arena.allocator(),
|
||||
.notification_arena = cdp.notification_arena.allocator(),
|
||||
@@ -585,7 +603,7 @@ pub const BrowserContext = struct {
|
||||
// It appends async tasks, so we make sure we run the message loop
|
||||
// before deinit it.
|
||||
env.inspector.?.resetContextGroup();
|
||||
env.inspector.?.stopSession();
|
||||
self.detachAllSessions();
|
||||
|
||||
// abort all intercepted requests before closing the session/page
|
||||
// since some of these might callback into the page/scriptmanager.
|
||||
@@ -622,6 +640,7 @@ pub const BrowserContext = struct {
|
||||
self.node_registry.deinit();
|
||||
self.node_search_list.deinit();
|
||||
self.set_child_nodes_sent.deinit(self.cdp.allocator);
|
||||
self.attached_sessions.deinit(self.cdp.allocator);
|
||||
|
||||
// Session.deinit (called via closeSession above) already cleared this
|
||||
// notification off any ownerless CorsGate/RobotsGate transfers.
|
||||
@@ -842,7 +861,7 @@ pub const BrowserContext = struct {
|
||||
pub fn closeTarget(self: *BrowserContext) !void {
|
||||
const target_id = self.target_id orelse return;
|
||||
const cdp = self.cdp;
|
||||
for (self.attached_sessions.items) |session| {
|
||||
for (self.attached_sessions.values()) |session| {
|
||||
self.fetchDisableForSession(session.id);
|
||||
try cdp.sendEvent("Inspector.detached", .{
|
||||
.reason = "Render process gone.",
|
||||
@@ -853,21 +872,7 @@ pub const BrowserContext = struct {
|
||||
.reason = "Render process gone.",
|
||||
}, .{ .session_id = session.parent_id });
|
||||
}
|
||||
self.attached_sessions.clearRetainingCapacity();
|
||||
|
||||
// could be null, created but never attached
|
||||
if (self.session_id) |session_id| {
|
||||
self.fetchDisableForSession(session_id);
|
||||
try cdp.sendEvent("Inspector.detached", .{
|
||||
.reason = "Render process gone.",
|
||||
}, .{ .session_id = session_id });
|
||||
try cdp.sendEvent("Target.detachedFromTarget", .{
|
||||
.targetId = target_id,
|
||||
.sessionId = session_id,
|
||||
.reason = "Render process gone.",
|
||||
}, .{});
|
||||
self.session_id = null;
|
||||
}
|
||||
self.detachAllSessions();
|
||||
|
||||
try cdp.sendEvent("Target.targetDestroyed", .{ .targetId = target_id }, .{});
|
||||
|
||||
@@ -1174,44 +1179,85 @@ pub const BrowserContext = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn callInspector(self: *const BrowserContext, msg: []const u8) void {
|
||||
self.inspector_session.send(msg);
|
||||
pub inline fn inspector(self: *const BrowserContext) *js.Inspector {
|
||||
return self.cdp.browser.env.inspector.?;
|
||||
}
|
||||
|
||||
/// Attaches `session_id` to the page target with its own inspector
|
||||
/// session. The id is copied into the context's arena.
|
||||
pub fn attachSession(self: *BrowserContext, session_id: []const u8, parent_id: ?[]const u8) !*AttachedSession {
|
||||
if (self.attached_sessions.contains(session_id)) {
|
||||
return error.SessionAlreadyAttached;
|
||||
}
|
||||
const allocator = self.cdp.allocator;
|
||||
|
||||
const attached = try allocator.create(AttachedSession);
|
||||
errdefer allocator.destroy(attached);
|
||||
|
||||
attached.* = .{
|
||||
.id = try self.arena.dupe(u8, session_id),
|
||||
.parent_id = parent_id,
|
||||
.bc = self,
|
||||
.inspector_session = undefined,
|
||||
};
|
||||
attached.inspector_session.init(self.inspector(), attached);
|
||||
errdefer attached.inspector_session.deinit();
|
||||
|
||||
try self.attached_sessions.put(allocator, attached.id, attached);
|
||||
return attached;
|
||||
}
|
||||
|
||||
/// The first session attached to the target.
|
||||
pub fn attachPrimarySession(self: *BrowserContext, session_id: []const u8) !*AttachedSession {
|
||||
lp.assert(self.session_id == null, "CDP.BrowserContext.attachPrimarySession already attached", .{});
|
||||
const attached = try self.attachSession(session_id, null);
|
||||
self.session_id = attached.id;
|
||||
return attached;
|
||||
}
|
||||
|
||||
/// Stops the session's inspector session and forgets it.
|
||||
/// Returns false when no such session is attached.
|
||||
pub fn detachSession(self: *BrowserContext, session_id: []const u8) bool {
|
||||
const kv = self.attached_sessions.fetchOrderedRemove(session_id) orelse return false;
|
||||
if (self.session_id) |primary| {
|
||||
if (std.mem.eql(u8, primary, session_id)) {
|
||||
self.session_id = null;
|
||||
}
|
||||
}
|
||||
self.destroySession(kv.value);
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn detachAllSessions(self: *BrowserContext) void {
|
||||
for (self.attached_sessions.values()) |attached| {
|
||||
self.destroySession(attached);
|
||||
}
|
||||
self.attached_sessions.clearRetainingCapacity();
|
||||
self.session_id = null;
|
||||
}
|
||||
|
||||
fn destroySession(self: *BrowserContext, attached: *AttachedSession) void {
|
||||
attached.inspector_session.deinit();
|
||||
self.cdp.allocator.destroy(attached);
|
||||
}
|
||||
|
||||
pub fn inspectorSession(self: *const BrowserContext, session_id: ?[]const u8) !*js.Inspector.Session {
|
||||
const id = session_id orelse self.session_id orelse return error.SessionNotAttached;
|
||||
const attached = self.attached_sessions.get(id) orelse return error.SessionNotAttached;
|
||||
return &attached.inspector_session;
|
||||
}
|
||||
|
||||
/// Forwards `cmd`'s raw JSON to the inspector session of the session it was sent through.
|
||||
pub fn callInspector(self: *BrowserContext, cmd: *const Command) !void {
|
||||
const inspector_session = try self.inspectorSession(cmd.input.session_id);
|
||||
inspector_session.send(cmd.input.json);
|
||||
self.session.browser.env.runMicrotasks();
|
||||
}
|
||||
|
||||
pub fn onInspectorResponse(ctx: *anyopaque, _: u32, msg: []const u8) void {
|
||||
sendInspectorMessage(@ptrCast(@alignCast(ctx)), msg) catch |err| {
|
||||
log.err(.cdp, "send inspector response", .{ .err = err });
|
||||
};
|
||||
}
|
||||
|
||||
pub fn onInspectorEvent(ctx: *anyopaque, msg: []const u8) void {
|
||||
if (log.enabled(.cdp, .debug)) {
|
||||
// msg should be {"method":<method>,...
|
||||
lp.assert(std.mem.startsWith(u8, msg, "{\"method\":"), "onInspectorEvent prefix", .{});
|
||||
const method_end = std.mem.indexOfScalar(u8, msg, ',') orelse {
|
||||
log.err(.cdp, "invalid inspector event", .{ .msg = msg });
|
||||
return;
|
||||
};
|
||||
const method = msg[10..method_end];
|
||||
log.debug(.cdp, "inspector event", .{ .method = method });
|
||||
}
|
||||
|
||||
sendInspectorMessage(@ptrCast(@alignCast(ctx)), msg) catch |err| {
|
||||
log.err(.cdp, "send inspector event", .{ .err = err });
|
||||
};
|
||||
}
|
||||
|
||||
// This is hacky x 2. First, we create the JSON payload by gluing our
|
||||
// This is hacky x 2. First, we create the JSON payload by gluing the
|
||||
// session_id onto it. Second, we're much more client/websocket aware than
|
||||
// we should be.
|
||||
fn sendInspectorMessage(self: *BrowserContext, msg: []const u8) !void {
|
||||
const session_id = self.session_id orelse {
|
||||
// We no longer have an active session. What should we do
|
||||
// in this case?
|
||||
return;
|
||||
};
|
||||
|
||||
fn sendInspectorMessage(self: *BrowserContext, msg: []const u8, session_id: []const u8) !void {
|
||||
const cdp = self.cdp;
|
||||
const allocator = cdp.link.acquireSendArena();
|
||||
defer cdp.link.releaseSendArena();
|
||||
|
||||
@@ -89,7 +89,7 @@ fn queryAXTree(cmd: *CDP.Command) !void {
|
||||
const params = (try cmd.params(Params)) orelse return error.InvalidParams;
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const node = try dom.getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId);
|
||||
const node = try dom.getNode(cmd.arena, bc, cmd.input.session_id, params.nodeId, params.backendNodeId, params.objectId);
|
||||
|
||||
const frame = bc.mainFrame() orelse return error.FrameNotLoaded;
|
||||
const temp_arena = try frame.getArena(.medium, "AXNode");
|
||||
@@ -124,7 +124,7 @@ fn getPartialAXTree(cmd: *CDP.Command) !void {
|
||||
}
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const node = try dom.getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId);
|
||||
const node = try dom.getNode(cmd.arena, bc, cmd.input.session_id, params.nodeId, params.backendNodeId, params.objectId);
|
||||
|
||||
const frame = bc.mainFrame() orelse return error.FrameNotLoaded;
|
||||
const temp_arena = try frame.getArena(.medium, "AXNode");
|
||||
|
||||
@@ -365,9 +365,12 @@ fn resolveNode(cmd: *CDP.Command) !void {
|
||||
js_context.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
// The object id is minted on the command's session; only that session can unwrap it later.
|
||||
const inspector_session = try bc.inspectorSession(cmd.input.session_id);
|
||||
|
||||
// node._node is a *DOMNode we need this to be able to find its most derived type e.g. Node -> Element -> HTMLElement
|
||||
// So we use the Node.Union when retrieve the value from the environment
|
||||
const remote_object = try bc.inspector_session.getRemoteObject(
|
||||
const remote_object = try inspector_session.getRemoteObject(
|
||||
&ls.local,
|
||||
params.objectGroup orelse "",
|
||||
node.dom,
|
||||
@@ -433,7 +436,7 @@ fn describeNode(cmd: *CDP.Command) !void {
|
||||
}
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
|
||||
const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId);
|
||||
const node = try getNode(cmd.arena, bc, cmd.input.session_id, params.nodeId, params.backendNodeId, params.objectId);
|
||||
|
||||
return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{ .depth = params.depth }) }, .{});
|
||||
}
|
||||
@@ -477,7 +480,7 @@ fn scrollIntoViewIfNeeded(cmd: *CDP.Command) !void {
|
||||
|
||||
// We retrieve the node to at least check if it exists and is valid.
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId);
|
||||
const node = try getNode(cmd.arena, bc, cmd.input.session_id, params.nodeId, params.backendNodeId, params.objectId);
|
||||
|
||||
switch (node.dom._type) {
|
||||
.element => {},
|
||||
@@ -489,7 +492,9 @@ fn scrollIntoViewIfNeeded(cmd: *CDP.Command) !void {
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
pub fn getNode(arena: Allocator, bc: *CDP.BrowserContext, node_id: ?NodeRegistry.Id, backend_node_id: ?NodeRegistry.Id, object_id: ?[]const u8) !*NodeRegistry.Node {
|
||||
/// `session_id` belongs to the command; a remote object id only resolves on
|
||||
/// the inspector session that minted it.
|
||||
pub fn getNode(arena: Allocator, bc: *CDP.BrowserContext, session_id: ?[]const u8, node_id: ?NodeRegistry.Id, backend_node_id: ?NodeRegistry.Id, object_id: ?[]const u8) !*NodeRegistry.Node {
|
||||
const input_node_id = node_id orelse backend_node_id;
|
||||
if (input_node_id) |input_node_id_| {
|
||||
return bc.node_registry.lookup_by_id.get(input_node_id_) orelse return error.NodeNotFound;
|
||||
@@ -501,7 +506,8 @@ pub fn getNode(arena: Allocator, bc: *CDP.BrowserContext, node_id: ?NodeRegistry
|
||||
defer ls.deinit();
|
||||
|
||||
// Retrieve the object from which ever context it is in.
|
||||
const parser_node = try bc.inspector_session.getNodePtr(arena, object_id_, &ls.local);
|
||||
const inspector_session = try bc.inspectorSession(session_id);
|
||||
const parser_node = try inspector_session.getNodePtr(arena, object_id_, &ls.local);
|
||||
return try bc.node_registry.register(@ptrCast(@alignCast(parser_node)));
|
||||
}
|
||||
return error.MissingParams;
|
||||
@@ -519,7 +525,7 @@ fn getContentQuads(cmd: *CDP.Command) !void {
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const frame = bc.mainFrame() orelse return error.FrameNotLoaded;
|
||||
|
||||
const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId);
|
||||
const node = try getNode(cmd.arena, bc, cmd.input.session_id, params.nodeId, params.backendNodeId, params.objectId);
|
||||
|
||||
// TODO likely if the following CSS properties are set the quads should be empty
|
||||
// visibility: hidden
|
||||
@@ -545,7 +551,7 @@ fn getBoxModel(cmd: *CDP.Command) !void {
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const frame = bc.mainFrame() orelse return error.FrameNotLoaded;
|
||||
|
||||
const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId);
|
||||
const node = try getNode(cmd.arena, bc, cmd.input.session_id, params.nodeId, params.backendNodeId, params.objectId);
|
||||
|
||||
// TODO implement for document or text
|
||||
const element = node.dom.is(DOMNode.Element) orelse return error.NodeIsNotAnElement;
|
||||
@@ -626,7 +632,7 @@ fn getOuterHTML(cmd: *CDP.Command) !void {
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const frame = bc.mainFrame() orelse return error.FrameNotLoaded;
|
||||
|
||||
const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId);
|
||||
const node = try getNode(cmd.arena, bc, cmd.input.session_id, params.nodeId, params.backendNodeId, params.objectId);
|
||||
|
||||
var aw = std.Io.Writer.Allocating.init(cmd.arena);
|
||||
try dump.deep(node.dom, .{}, &aw.writer, frame);
|
||||
@@ -640,7 +646,7 @@ fn requestNode(cmd: *CDP.Command) !void {
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const node = try getNode(cmd.arena, bc, null, null, params.objectId);
|
||||
const node = try getNode(cmd.arena, bc, cmd.input.session_id, null, null, params.objectId);
|
||||
|
||||
return cmd.sendResult(.{ .nodeId = node.id }, .{});
|
||||
}
|
||||
@@ -661,7 +667,7 @@ fn setFileInputFiles(cmd: *CDP.Command) !void {
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const root = bc.mainFrame() orelse return error.FrameNotLoaded;
|
||||
|
||||
const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId);
|
||||
const node = try getNode(cmd.arena, bc, cmd.input.session_id, params.nodeId, params.backendNodeId, params.objectId);
|
||||
const element = node.dom.is(DOMNode.Element) orelse return error.NodeIsNotAnElement;
|
||||
const input = element.is(Input) orelse return error.NotAnInputElement;
|
||||
if (input._input_type != .file) return error.NotAFileInput;
|
||||
@@ -694,7 +700,7 @@ fn focus(cmd: *CDP.Command) !void {
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const frame = bc.mainFrame() orelse return error.FrameNotLoaded;
|
||||
|
||||
const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId);
|
||||
const node = try getNode(cmd.arena, bc, cmd.input.session_id, params.nodeId, params.backendNodeId, params.objectId);
|
||||
const element = node.dom.is(DOMNode.Element) orelse return error.NodeIsNotAnElement;
|
||||
if (element.isFocusable(frame) == false) {
|
||||
return cmd.sendError(-32000, "Element is not focusable", .{});
|
||||
@@ -1400,7 +1406,49 @@ fn mainWorldContextId(bc: *CDP.BrowserContext, frame: *const Frame) !i32 {
|
||||
var ls: js.Local.Scope = undefined;
|
||||
frame.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
return bc.inspector_session.inspector.getContextId(&ls.local);
|
||||
return bc.inspector().getContextId(&ls.local);
|
||||
}
|
||||
|
||||
test "cdp.dom: remote object ids belong to the session that minted them" {
|
||||
var ctx = try testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
const bc = try ctx.loadBrowserContext(.{ .id = "BID-RO", .url = "cdp/dom1.html", .target_id = "FID-000000000R".*, .session_id = "SID-PRIMARY" });
|
||||
_ = try bc.attachSession("SID-AUX", null);
|
||||
|
||||
const root = bc.mainFrame() orelse unreachable;
|
||||
const html = root.document.getDocumentElement() orelse unreachable;
|
||||
const html_node = try bc.node_registry.register(html.asNode());
|
||||
const document_node = try bc.node_registry.register(root.document.asNode());
|
||||
|
||||
// The auxiliary session mints an id for <html> and resolves it.
|
||||
try ctx.processMessage(.{ .id = 20, .method = "DOM.resolveNode", .sessionId = "SID-AUX", .params = .{ .backendNodeId = html_node.id } });
|
||||
const aux_object_id = try sentObjectId(&ctx, 20);
|
||||
try ctx.processMessage(.{ .id = 21, .method = "DOM.requestNode", .sessionId = "SID-AUX", .params = .{ .objectId = aux_object_id } });
|
||||
try ctx.expectSentResult(.{ .nodeId = html_node.id }, .{ .id = 21, .session_id = "SID-AUX" });
|
||||
|
||||
// The primary has minted nothing yet: the auxiliary's id is not its.
|
||||
try ctx.processMessage(.{ .id = 22, .method = "Runtime.callFunctionOn", .sessionId = "SID-PRIMARY", .params = .{
|
||||
.objectId = aux_object_id,
|
||||
.functionDeclaration = "function() { return this.localName; }",
|
||||
.returnByValue = true,
|
||||
} });
|
||||
try ctx.expectSentError(-32000, "Could not find object with given id", .{ .id = 22 });
|
||||
|
||||
// Each session numbers its ids by itself, so the primary's first id may
|
||||
// well be the same string as the auxiliary's: the session a command comes
|
||||
// through, not the id, tells the objects apart. Mint a different node on
|
||||
// the primary and check that each id describes its own.
|
||||
try ctx.processMessage(.{ .id = 23, .method = "DOM.resolveNode", .sessionId = "SID-PRIMARY", .params = .{ .backendNodeId = document_node.id } });
|
||||
const primary_object_id = try sentObjectId(&ctx, 23);
|
||||
try ctx.processMessage(.{ .id = 24, .method = "DOM.describeNode", .sessionId = "SID-PRIMARY", .params = .{ .objectId = primary_object_id } });
|
||||
try ctx.expectSentResult(.{ .node = .{ .nodeId = document_node.id, .nodeName = "#document" } }, .{ .id = 24, .session_id = "SID-PRIMARY" });
|
||||
try ctx.processMessage(.{ .id = 25, .method = "DOM.describeNode", .sessionId = "SID-AUX", .params = .{ .objectId = aux_object_id } });
|
||||
try ctx.expectSentResult(.{ .node = .{ .nodeId = html_node.id, .localName = "html" } }, .{ .id = 25, .session_id = "SID-AUX" });
|
||||
try ctx.processMessage(.{ .id = 26, .method = "DOM.requestNode", .sessionId = "SID-PRIMARY", .params = .{ .objectId = primary_object_id } });
|
||||
try ctx.expectSentResult(.{ .nodeId = document_node.id }, .{ .id = 26, .session_id = "SID-PRIMARY" });
|
||||
try ctx.processMessage(.{ .id = 27, .method = "DOM.requestNode", .sessionId = "SID-AUX", .params = .{ .objectId = aux_object_id } });
|
||||
try ctx.expectSentResult(.{ .nodeId = html_node.id }, .{ .id = 27, .session_id = "SID-AUX" });
|
||||
}
|
||||
|
||||
// The result.object.objectId of the response to command `msg_id`.
|
||||
|
||||
@@ -595,10 +595,7 @@ test "cdp.Fetch: interception events belong to the enabling session" {
|
||||
.session_id = "SID-PRIMARY",
|
||||
.target_id = "TID-000000000B".*,
|
||||
});
|
||||
try bc.attached_sessions.append(bc.arena, .{
|
||||
.id = "SID-AUX",
|
||||
.parent_id = null,
|
||||
});
|
||||
_ = try bc.attachSession("SID-AUX", null);
|
||||
|
||||
try ctx.processMessage(.{
|
||||
.id = 1,
|
||||
|
||||
@@ -1591,7 +1591,7 @@ test "cdp.Network: worker requests emit network events" {
|
||||
_ = try cdp.createBrowserContext();
|
||||
var bc = &cdp.browser_context.?;
|
||||
bc.id = "BID-NW";
|
||||
bc.session_id = "SID-NW";
|
||||
_ = try bc.attachPrimarySession("SID-NW");
|
||||
bc.target_id = "TID-NW-0000000".*;
|
||||
|
||||
try ctx.processMessage(.{ .id = 1, .method = "Network.enable" });
|
||||
|
||||
@@ -284,7 +284,7 @@ fn createIsolatedWorld(cmd: *CDP.Command) !void {
|
||||
var ls: js.Local.Scope = undefined;
|
||||
js_context.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
const context_id = bc.inspector_session.inspector.getContextId(&ls.local);
|
||||
const context_id = bc.inspector().getContextId(&ls.local);
|
||||
return cmd.sendResult(.{ .executionContextId = context_id }, .{});
|
||||
}
|
||||
|
||||
@@ -315,7 +315,7 @@ fn registerIsolatedWorldContext(arena: Allocator, bc: *CDP.BrowserContext, world
|
||||
js_context.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
bc.inspector_session.inspector.contextCreated(
|
||||
bc.inspector().contextCreated(
|
||||
&ls.local,
|
||||
world.name,
|
||||
frame.origin orelse "",
|
||||
@@ -558,7 +558,7 @@ pub fn frameNavigate(bc: *CDP.BrowserContext, event: *const Notification.FrameNa
|
||||
pub fn frameRemove(bc: *CDP.BrowserContext) void {
|
||||
// Clear all remote object mappings to prevent stale objectIds from being used
|
||||
// after the context is destroy
|
||||
bc.inspector_session.inspector.resetContextGroup();
|
||||
bc.inspector().resetContextGroup();
|
||||
|
||||
// The main frame is going to be removed, we need to remove contexts from other worlds first.
|
||||
for (bc.isolated_worlds.items) |isolated_world| {
|
||||
@@ -743,7 +743,7 @@ pub fn frameNavigated(arena: Allocator, bc: *CDP.BrowserContext, event: *const N
|
||||
frame.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
bc.inspector_session.inspector.contextCreated(
|
||||
bc.inspector().contextCreated(
|
||||
&ls.local,
|
||||
"",
|
||||
frame.origin orelse "",
|
||||
@@ -1735,7 +1735,7 @@ fn isolatedWorldContextId(bc: *CDP.BrowserContext, frame: *const Frame) !i32 {
|
||||
var ls: js.Local.Scope = undefined;
|
||||
js_context.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
return bc.inspector_session.inspector.getContextId(&ls.local);
|
||||
return bc.inspector().getContextId(&ls.local);
|
||||
}
|
||||
|
||||
test "cdp.frame: child frame metadata" {
|
||||
@@ -2170,7 +2170,7 @@ test "cdp.frame: reload replays POST navigation" {
|
||||
_ = try cdp_inst.createBrowserContext();
|
||||
var bc = &cdp_inst.browser_context.?;
|
||||
bc.id = "BID-A6";
|
||||
bc.session_id = "SID-X";
|
||||
_ = try bc.attachPrimarySession("SID-X");
|
||||
bc.target_id = "TID-A6-0000000".*;
|
||||
|
||||
// First navigation: POST a form-style payload to /echo_method.
|
||||
@@ -2222,7 +2222,7 @@ test "cdp.frame: reload after POST→redirect drops the POST" {
|
||||
_ = try cdp_inst.createBrowserContext();
|
||||
var bc = &cdp_inst.browser_context.?;
|
||||
bc.id = "BID-A6R";
|
||||
bc.session_id = "SID-XR";
|
||||
_ = try bc.attachPrimarySession("SID-XR");
|
||||
bc.target_id = "TID-A6R-000000".*;
|
||||
|
||||
// First navigation: POST /redirect_to_echo → 302 → GET /echo_method.
|
||||
@@ -2486,7 +2486,7 @@ test "cdp.frame: first navigation of a pristine bootstrap about:blank navigates
|
||||
defer ctx.deinit();
|
||||
|
||||
var bc = try ctx.loadBrowserContext(.{ .id = "BID-PRS", .target_id = "TID-PRS-000000".* });
|
||||
bc.session_id = "SID-PRS";
|
||||
_ = try bc.attachPrimarySession("SID-PRS");
|
||||
_ = try bc.session.createPage();
|
||||
const before = bc.mainFrame() orelse unreachable;
|
||||
try testing.expectEqualSlices(u8, "about:blank", before.url);
|
||||
@@ -2515,7 +2515,7 @@ test "cdp.frame: anchor click sends Referer matching the originating page" {
|
||||
_ = try cdp_inst.createBrowserContext();
|
||||
var bc = &cdp_inst.browser_context.?;
|
||||
bc.id = "BID-A18";
|
||||
bc.session_id = "SID-A18";
|
||||
_ = try bc.attachPrimarySession("SID-A18");
|
||||
bc.target_id = "TID-A18-000000".*;
|
||||
|
||||
// Initial navigation to the page hosting the anchor — driven directly via
|
||||
@@ -2564,7 +2564,7 @@ test "cdp.frame: address-bar Page.navigate sends no Referer" {
|
||||
_ = try cdp_inst.createBrowserContext();
|
||||
var bc = &cdp_inst.browser_context.?;
|
||||
bc.id = "BID-A18B";
|
||||
bc.session_id = "SID-A18B";
|
||||
_ = try bc.attachPrimarySession("SID-A18B");
|
||||
bc.target_id = "TID-A18B-00000".*;
|
||||
|
||||
{
|
||||
|
||||
@@ -80,7 +80,7 @@ fn sendInspector(cmd: *CDP.Command) !void {
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
|
||||
// the result to return is handled directly by the inspector.
|
||||
bc.callInspector(cmd.input.json);
|
||||
try bc.callInspector(cmd);
|
||||
}
|
||||
|
||||
// Object arguments stay remote handles; serializing them would execute page JS.
|
||||
@@ -127,18 +127,21 @@ const ConsoleMessage = struct {
|
||||
};
|
||||
|
||||
pub fn consoleMessage(arena: Allocator, bc: *CDP.BrowserContext, event: *const Notification.ConsoleMessage) !void {
|
||||
// The event goes to the primary session, so its argument handles are
|
||||
// minted on the primary's inspector session.
|
||||
const session_id = bc.session_id orelse return;
|
||||
const inspector_session = try bc.inspectorSession(session_id);
|
||||
const frame = bc.mainFrame() orelse return error.FrameNotLoaded;
|
||||
|
||||
var ls: js.Local.Scope = undefined;
|
||||
frame.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
const context_id = bc.inspector_session.inspector.getContextId(&ls.local);
|
||||
const context_id = bc.inspector().getContextId(&ls.local);
|
||||
|
||||
var args: std.ArrayList(RemoteObject) = .empty;
|
||||
for (event.values) |value| {
|
||||
const remote_object = try bc.inspector_session.getRemoteObject(
|
||||
const remote_object = try inspector_session.getRemoteObject(
|
||||
&ls.local,
|
||||
"",
|
||||
value,
|
||||
@@ -180,6 +183,77 @@ test "cdp.runtime: inspector-handled methods pass through" {
|
||||
try ctx.expectSentResult(null, .{ .id = 52 });
|
||||
}
|
||||
|
||||
// Playwright's browserContext.newCDPSession attaches a second session to the
|
||||
// page and sends Runtime commands through it. The inspector's answer must
|
||||
// carry that session's id: the driver keys its pending callbacks by session
|
||||
// and asserts on a response it can't match (lightpanda-io/browser#1838).
|
||||
test "cdp.runtime: inspector responses go to the session that sent the command" {
|
||||
var ctx = try testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
const bc = try ctx.loadBrowserContext(.{ .id = "BID-RT2", .url = "hi.html", .target_id = "FID-0000000RT2".*, .session_id = "SID-PRIMARY" });
|
||||
_ = try bc.attachSession("SID-AUX", null);
|
||||
|
||||
try ctx.processMessage(.{ .id = 60, .method = "Runtime.evaluate", .sessionId = "SID-AUX", .params = .{ .expression = "1 + 1", .returnByValue = true } });
|
||||
try ctx.expectSentResult(.{ .result = .{ .type = "number", .value = 2, .description = "2" } }, .{ .id = 60, .session_id = "SID-AUX" });
|
||||
|
||||
// The primary session keeps working as before.
|
||||
try ctx.processMessage(.{ .id = 61, .method = "Runtime.evaluate", .sessionId = "SID-PRIMARY", .params = .{ .expression = "2 + 2", .returnByValue = true } });
|
||||
try ctx.expectSentResult(.{ .result = .{ .type = "number", .value = 4, .description = "4" } }, .{ .id = 61, .session_id = "SID-PRIMARY" });
|
||||
|
||||
// A response the inspector produces after the dispatch (awaitPromise
|
||||
// answers from a microtask) still finds its session.
|
||||
try ctx.processMessage(.{ .id = 62, .method = "Runtime.evaluate", .sessionId = "SID-AUX", .params = .{ .expression = "Promise.resolve('late')", .awaitPromise = true, .returnByValue = true } });
|
||||
try ctx.expectSentResult(.{ .result = .{ .type = "string", .value = "late" } }, .{ .id = 62, .session_id = "SID-AUX" });
|
||||
|
||||
// A command with no sessionId is answered on the primary session.
|
||||
try ctx.processMessage(.{ .id = 63, .method = "Runtime.evaluate", .params = .{ .expression = "3 + 3", .returnByValue = true } });
|
||||
try ctx.expectSentResult(.{ .result = .{ .type = "number", .value = 6, .description = "6" } }, .{ .id = 63, .session_id = "SID-PRIMARY" });
|
||||
}
|
||||
|
||||
// Number of `method` events the client received on `session_id`.
|
||||
fn countSentEvents(ctx: *testing.TestContext, method: []const u8, session_id: []const u8) !usize {
|
||||
var count: usize = 0;
|
||||
var i: usize = 0;
|
||||
while (try ctx.getSentMessage(i)) |msg| : (i += 1) {
|
||||
const obj = switch (msg) {
|
||||
.object => |o| o,
|
||||
else => continue,
|
||||
};
|
||||
const sent_method = obj.get("method") orelse continue;
|
||||
if (sent_method != .string or !std.mem.eql(u8, sent_method.string, method)) {
|
||||
continue;
|
||||
}
|
||||
const sent_session_id = obj.get("sessionId") orelse continue;
|
||||
if (sent_session_id == .string and std.mem.eql(u8, sent_session_id.string, session_id)) {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// V8 keeps Runtime.enable per inspector session: a session only gets the
|
||||
// executionContextCreated events it asked for, stamped with its own id.
|
||||
test "cdp.runtime: inspector events go to the session that enabled them" {
|
||||
var ctx = try testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
const bc = try ctx.loadBrowserContext(.{ .id = "BID-RT3", .url = "hi.html", .target_id = "FID-0000000RT3".*, .session_id = "SID-PRIMARY" });
|
||||
_ = try bc.attachSession("SID-AUX", null);
|
||||
|
||||
try ctx.processMessage(.{ .id = 70, .method = "Runtime.enable", .sessionId = "SID-AUX" });
|
||||
try ctx.expectSentResult(null, .{ .id = 70, .session_id = "SID-AUX" });
|
||||
try ctx.expectSentEvent("Runtime.executionContextCreated", .{ .context = .{ .auxData = .{ .isDefault = true, .type = "default" } } }, .{ .session_id = "SID-AUX" });
|
||||
try testing.expectEqual(1, try countSentEvents(&ctx, "Runtime.executionContextCreated", "SID-AUX"));
|
||||
try testing.expectEqual(0, try countSentEvents(&ctx, "Runtime.executionContextCreated", "SID-PRIMARY"));
|
||||
|
||||
try ctx.processMessage(.{ .id = 71, .method = "Runtime.enable", .sessionId = "SID-PRIMARY" });
|
||||
try ctx.expectSentResult(null, .{ .id = 71, .session_id = "SID-PRIMARY" });
|
||||
try ctx.expectSentEvent("Runtime.executionContextCreated", .{ .context = .{ .auxData = .{ .isDefault = true, .type = "default" } } }, .{ .session_id = "SID-PRIMARY" });
|
||||
try testing.expectEqual(1, try countSentEvents(&ctx, "Runtime.executionContextCreated", "SID-AUX"));
|
||||
try testing.expectEqual(1, try countSentEvents(&ctx, "Runtime.executionContextCreated", "SID-PRIMARY"));
|
||||
}
|
||||
|
||||
test "cdp.runtime: consoleAPICalled type matches the console method" {
|
||||
testing.silenceLog(&.{.js});
|
||||
|
||||
|
||||
@@ -196,7 +196,7 @@ fn createTarget(cmd: *CDP.Command) !void {
|
||||
defer ls.deinit();
|
||||
|
||||
const aux_data = try std.fmt.allocPrint(cmd.arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{target_id});
|
||||
bc.inspector_session.inspector.contextCreated(
|
||||
bc.inspector().contextCreated(
|
||||
&ls.local,
|
||||
"",
|
||||
"", // @ZIGDOM
|
||||
@@ -267,14 +267,10 @@ fn attachToTarget(cmd: *CDP.Command) !void {
|
||||
cmd.cdp.resolveSessionId(session_id) orelse return error.UnknownSessionId
|
||||
else
|
||||
null;
|
||||
const session_id = try bc.arena.dupe(u8, cmd.cdp.session_id_gen.next());
|
||||
try bc.attached_sessions.append(bc.arena, .{
|
||||
.id = session_id,
|
||||
.parent_id = parent_id,
|
||||
});
|
||||
const session = try bc.attachSession(cmd.cdp.session_id_gen.next(), parent_id);
|
||||
|
||||
try cmd.sendEvent("Target.attachedToTarget", AttachToTarget{
|
||||
.sessionId = session_id,
|
||||
.sessionId = session.id,
|
||||
.targetInfo = TargetInfo{
|
||||
.targetId = target_id,
|
||||
.title = bc.getTitle() orelse "",
|
||||
@@ -283,7 +279,7 @@ fn attachToTarget(cmd: *CDP.Command) !void {
|
||||
},
|
||||
}, .{ .session_id = parent_id });
|
||||
|
||||
return cmd.sendResult(.{ .sessionId = session_id }, .{});
|
||||
return cmd.sendResult(.{ .sessionId = session.id }, .{});
|
||||
}
|
||||
|
||||
fn attachToBrowserTarget(cmd: *CDP.Command) !void {
|
||||
@@ -402,31 +398,18 @@ fn detachFromTarget(cmd: *CDP.Command) !void {
|
||||
const params = (try cmd.params(Params)) orelse Params{};
|
||||
|
||||
if (cmd.browser_context) |bc| {
|
||||
if (params.sessionId) |requested_session_id| {
|
||||
for (bc.attached_sessions.items, 0..) |session, index| {
|
||||
if (!std.mem.eql(u8, session.id, requested_session_id)) continue;
|
||||
// Without a sessionId, detach the primary session (if any).
|
||||
if (params.sessionId orelse bc.session_id) |requested_session_id| {
|
||||
const session = bc.attached_sessions.get(requested_session_id) orelse return error.UnknownSessionId;
|
||||
const session_id = session.id;
|
||||
const parent_id = session.parent_id;
|
||||
|
||||
_ = bc.attached_sessions.orderedRemove(index);
|
||||
bc.fetchDisableForSession(session.id);
|
||||
try cmd.sendEvent("Target.detachedFromTarget", .{
|
||||
.sessionId = session.id,
|
||||
}, .{ .session_id = session.parent_id });
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
const session_id = bc.session_id orelse return error.UnknownSessionId;
|
||||
if (!std.mem.eql(u8, session_id, requested_session_id)) {
|
||||
return error.UnknownSessionId;
|
||||
}
|
||||
}
|
||||
|
||||
if (bc.session_id) |session_id| {
|
||||
bc.fetchDisableForSession(session_id);
|
||||
try cmd.sendEvent("Target.detachedFromTarget", .{
|
||||
.sessionId = session_id,
|
||||
}, .{});
|
||||
}, .{ .session_id = parent_id });
|
||||
_ = bc.detachSession(session_id);
|
||||
}
|
||||
bc.session_id = null;
|
||||
}
|
||||
|
||||
return cmd.sendResult(null, .{});
|
||||
@@ -456,8 +439,8 @@ fn setAutoAttach(cmd: *CDP.Command) !void {
|
||||
try cmd.sendEvent("Target.detachedFromTarget", .{
|
||||
.sessionId = session_id,
|
||||
}, .{});
|
||||
_ = bc.detachSession(session_id);
|
||||
}
|
||||
bc.session_id = null;
|
||||
}
|
||||
try cmd.sendResult(null, .{});
|
||||
return;
|
||||
@@ -500,27 +483,24 @@ fn setAutoAttach(cmd: *CDP.Command) !void {
|
||||
|
||||
fn doAttachtoTarget(cmd: *CDP.Command, target_id: []const u8) !void {
|
||||
const bc = cmd.browser_context.?;
|
||||
const session_id = bc.session_id orelse blk: {
|
||||
break :blk try bc.arena.dupe(u8, cmd.cdp.session_id_gen.next());
|
||||
};
|
||||
const parent_id = bc.session_id;
|
||||
|
||||
if (bc.session_id == null) {
|
||||
// extra_headers should not be kept on a new frame or tab,
|
||||
// currently we have only 1 frame, we clear it just in case
|
||||
bc.extra_headers.clearRetainingCapacity();
|
||||
_ = try bc.attachPrimarySession(cmd.cdp.session_id_gen.next());
|
||||
}
|
||||
|
||||
try cmd.sendEvent("Target.attachedToTarget", AttachToTarget{
|
||||
.sessionId = session_id,
|
||||
.sessionId = bc.session_id.?,
|
||||
.targetInfo = TargetInfo{
|
||||
.targetId = target_id,
|
||||
.title = bc.getTitle() orelse "",
|
||||
.url = bc.getURL() orelse "about:blank",
|
||||
.browserContextId = bc.id,
|
||||
},
|
||||
}, .{ .session_id = bc.session_id });
|
||||
|
||||
bc.session_id = session_id;
|
||||
}, .{ .session_id = parent_id });
|
||||
}
|
||||
|
||||
const AttachToTarget = struct {
|
||||
@@ -629,7 +609,9 @@ test "cdp.target: disposeBrowserContext detaches target sessions" {
|
||||
.sessionId = "BSID-1",
|
||||
.params = .{ .targetId = target_id },
|
||||
});
|
||||
const auxiliary_id = try testing.arena_allocator.dupe(u8, bc.attached_sessions.items[0].id);
|
||||
// the primary is attached first, the auxiliary session after it
|
||||
try testing.expectEqual(2, bc.attached_sessions.count());
|
||||
const auxiliary_id = try testing.arena_allocator.dupe(u8, bc.attached_sessions.keys()[1]);
|
||||
|
||||
try ctx.processMessage(.{
|
||||
.id = 5,
|
||||
@@ -909,7 +891,8 @@ test "cdp.target: attachToTarget" {
|
||||
const session_id = bc.session_id.?;
|
||||
try ctx.expectSentResult(.{ .sessionId = session_id }, .{ .id = 11 });
|
||||
try ctx.expectSentEvent("Target.attachedToTarget", .{ .sessionId = session_id, .targetInfo = .{ .url = "about:blank", .title = "", .attached = true, .type = "page", .canAccessOpener = false, .browserContextId = "BID-9", .targetId = bc.target_id.? } }, .{});
|
||||
try testing.expectEqual(0, bc.attached_sessions.items.len);
|
||||
try testing.expectEqual(1, bc.attached_sessions.count());
|
||||
try testing.expect(bc.attached_sessions.contains(session_id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -930,7 +913,9 @@ test "cdp.target: auxiliary session is unique and routed through its parent" {
|
||||
.params = .{ .targetId = "TID-000000000B" },
|
||||
});
|
||||
|
||||
const session_id = bc.attached_sessions.items[0].id;
|
||||
// the primary is attached first, the auxiliary session after it
|
||||
try testing.expectEqual(2, bc.attached_sessions.count());
|
||||
const session_id = bc.attached_sessions.keys()[1];
|
||||
try testing.expect(!std.mem.eql(u8, session_id, bc.session_id.?));
|
||||
try ctx.expectSentEvent("Target.attachedToTarget", .{
|
||||
.sessionId = session_id,
|
||||
@@ -1045,16 +1030,78 @@ test "cdp.target: detachFromTarget auxiliary session" {
|
||||
});
|
||||
|
||||
try ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-000000000B" } });
|
||||
const session_id = bc.attached_sessions.items[0].id;
|
||||
try testing.expectEqual(2, bc.attached_sessions.count());
|
||||
const session_id = bc.attached_sessions.keys()[1];
|
||||
try testing.expect(!std.mem.eql(u8, session_id, bc.session_id.?));
|
||||
|
||||
try ctx.processMessage(.{ .id = 11, .method = "Target.detachFromTarget", .params = .{ .sessionId = session_id } });
|
||||
try ctx.expectSentEvent("Target.detachedFromTarget", .{ .sessionId = session_id }, .{});
|
||||
try testing.expectEqual(0, bc.attached_sessions.items.len);
|
||||
try testing.expectEqual(1, bc.attached_sessions.count());
|
||||
try testing.expectEqual(true, bc.session_id != null);
|
||||
try ctx.expectSentResult(null, .{ .id = 11 });
|
||||
}
|
||||
|
||||
// A detached session's inspector session goes with it: commands on the old
|
||||
// id are rejected up front and the primary is unaffected.
|
||||
test "cdp.target: detachFromTarget releases the auxiliary session's inspector session" {
|
||||
var ctx = try testing.context();
|
||||
defer ctx.deinit();
|
||||
const bc = try ctx.loadBrowserContext(.{
|
||||
.id = "BID-9",
|
||||
.url = "hi.html",
|
||||
.session_id = "SID-PRIMARY",
|
||||
.target_id = "TID-000000000B".*,
|
||||
});
|
||||
_ = try bc.attachSession("SID-AUX", null);
|
||||
|
||||
try ctx.processMessage(.{ .id = 10, .method = "Runtime.evaluate", .sessionId = "SID-AUX", .params = .{ .expression = "1 + 1", .returnByValue = true } });
|
||||
try ctx.expectSentResult(.{ .result = .{ .type = "number", .value = 2 } }, .{ .id = 10, .session_id = "SID-AUX" });
|
||||
|
||||
try ctx.processMessage(.{ .id = 11, .method = "Target.detachFromTarget", .params = .{ .sessionId = "SID-AUX" } });
|
||||
try ctx.expectSentEvent("Target.detachedFromTarget", .{ .sessionId = "SID-AUX" }, .{});
|
||||
try ctx.expectSentResult(null, .{ .id = 11 });
|
||||
try testing.expectEqual(1, bc.attached_sessions.count());
|
||||
|
||||
try ctx.processMessage(.{ .id = 12, .method = "Runtime.evaluate", .sessionId = "SID-AUX", .params = .{ .expression = "1 + 1", .returnByValue = true } });
|
||||
try ctx.expectSentError(-32001, "Unknown sessionId", .{ .id = 12 });
|
||||
|
||||
try ctx.processMessage(.{ .id = 13, .method = "Runtime.evaluate", .sessionId = "SID-PRIMARY", .params = .{ .expression = "2 + 2", .returnByValue = true } });
|
||||
try ctx.expectSentResult(.{ .result = .{ .type = "number", .value = 4 } }, .{ .id = 13, .session_id = "SID-PRIMARY" });
|
||||
}
|
||||
|
||||
// Stopping an inspector session fails its pending evaluations. The client was
|
||||
// already told the session is detached, so, like Chrome, nothing more is sent.
|
||||
test "cdp.target: detachFromTarget drops the auxiliary session's pending responses" {
|
||||
var ctx = try testing.context();
|
||||
defer ctx.deinit();
|
||||
const bc = try ctx.loadBrowserContext(.{
|
||||
.id = "BID-9",
|
||||
.url = "hi.html",
|
||||
.session_id = "SID-PRIMARY",
|
||||
.target_id = "TID-000000000B".*,
|
||||
});
|
||||
_ = try bc.attachSession("SID-AUX", null);
|
||||
|
||||
try ctx.processMessage(.{ .id = 10, .method = "Runtime.enable", .sessionId = "SID-AUX" });
|
||||
try ctx.expectSentResult(null, .{ .id = 10, .session_id = "SID-AUX" });
|
||||
try ctx.processMessage(.{ .id = 11, .method = "Runtime.evaluate", .sessionId = "SID-AUX", .params = .{ .expression = "({a: 1})" } });
|
||||
try ctx.expectSentResult(.{ .result = .{ .type = "object" } }, .{ .id = 11, .session_id = "SID-AUX" });
|
||||
try ctx.processMessage(.{ .id = 12, .method = "Runtime.evaluate", .sessionId = "SID-AUX", .params = .{ .expression = "new Promise(() => {})", .awaitPromise = true } });
|
||||
|
||||
try ctx.processMessage(.{ .id = 13, .method = "Target.detachFromTarget", .params = .{ .sessionId = "SID-AUX" } });
|
||||
try ctx.expectSentEvent("Target.detachedFromTarget", .{ .sessionId = "SID-AUX" }, .{});
|
||||
try ctx.expectSentResult(null, .{ .id = 13 });
|
||||
|
||||
var i: usize = 0;
|
||||
while (try ctx.getSentMessage(i)) |msg| : (i += 1) {
|
||||
const msg_id = msg.object.get("id") orelse continue;
|
||||
try testing.expect(msg_id != .integer or msg_id.integer != 12);
|
||||
}
|
||||
|
||||
try ctx.processMessage(.{ .id = 14, .method = "Runtime.evaluate", .sessionId = "SID-PRIMARY", .params = .{ .expression = "2 + 2", .returnByValue = true } });
|
||||
try ctx.expectSentResult(.{ .result = .{ .type = "number", .value = 4 } }, .{ .id = 14, .session_id = "SID-PRIMARY" });
|
||||
}
|
||||
|
||||
test "cdp.target: detachFromTarget without session" {
|
||||
var ctx = try testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
@@ -95,12 +95,12 @@ pub const TestContext = struct {
|
||||
}
|
||||
|
||||
if (opts.session_id) |sid| {
|
||||
bc.session_id = sid;
|
||||
_ = try bc.attachPrimarySession(sid);
|
||||
}
|
||||
|
||||
if (opts.url) |url| {
|
||||
if (bc.session_id == null) {
|
||||
bc.session_id = "SID-X";
|
||||
_ = try bc.attachPrimarySession("SID-X");
|
||||
}
|
||||
if (bc.target_id == null) {
|
||||
bc.target_id = "TID-000000000Z".*;
|
||||
|
||||
Reference in new issue
Block a user