Reorganize v8 contexts and scope

- Pages within the same session have proper isolation
  - they have their own window
  - they have their own SessionState
  - they have their own v8.Context

- Move inspector to CDP browser context
  - Browser now knows nothing about the inspector

- Use notification to emit a context-created message
  - This is still a bit hacky, but again, it decouples browser from CDP
This commit is contained in:
Karl Seguin
2025-04-28 21:04:01 +08:00
parent 0fb0532875
commit 2d5ff8252c
19 changed files with 1213 additions and 1236 deletions

View File

@@ -127,24 +127,27 @@ fn resolveNode(cmd: anytype) !void {
objectGroup: ?[]const u8 = null,
executionContextId: ?u32 = null,
})) orelse return error.InvalidParams;
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
var executor = bc.session.executor;
var scope = page.scope;
if (params.executionContextId) |context_id| {
if (executor.context.debugContextId() != context_id) {
if (scope.context.debugContextId() != context_id) {
const isolated_world = bc.isolated_world orelse return error.ContextNotFound;
executor = isolated_world.executor;
scope = isolated_world.scope;
if (executor.context.debugContextId() != context_id) return error.ContextNotFound;
if (scope.context.debugContextId() != context_id) return error.ContextNotFound;
}
}
const input_node_id = if (params.nodeId) |node_id| node_id else params.backendNodeId orelse return error.InvalidParams;
const input_node_id = params.nodeId orelse params.backendNodeId orelse return error.InvalidParam;
const node = bc.node_registry.lookup_by_id.get(input_node_id) orelse return error.UnknownNode;
// node._node is a *parser.Node 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.session.inspector.getRemoteObject(
executor,
const remote_object = try bc.inspector.getRemoteObject(
scope,
params.objectGroup orelse "",
try dom_node.Node.toInterface(node._node),
);
@@ -163,28 +166,61 @@ fn resolveNode(cmd: anytype) !void {
fn describeNode(cmd: anytype) !void {
const params = (try cmd.params(struct {
nodeId: ?Node.Id = null,
backendNodeId: ?Node.Id = null,
objectId: ?[]const u8 = null,
depth: u32 = 1,
pierce: bool = false,
backendNodeId: ?u32 = null,
objectGroup: ?[]const u8 = null,
executionContextId: ?u32 = null,
})) orelse return error.InvalidParams;
if (params.backendNodeId != null or params.depth != 1 or params.pierce) {
if (params.nodeId == null or params.backendNodeId != null or params.executionContextId != null) {
return error.NotYetImplementedParams;
}
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
const node = bc.node_registry.lookup_by_id.get(params.nodeId.?) orelse return error.UnknownNode;
if (params.nodeId != null) {
const node = bc.node_registry.lookup_by_id.get(params.nodeId.?) orelse return error.NodeNotFound;
return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{}) }, .{});
}
if (params.objectId != null) {
// Retrieve the object from which ever context it is in.
const parser_node = try bc.session.inspector.getNodePtr(cmd.arena, params.objectId.?);
const node = try bc.node_registry.register(@ptrCast(parser_node));
return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{}) }, .{});
}
return error.MissingParams;
// node._node is a *parser.Node 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.getRemoteObject(
page.scope,
params.objectGroup orelse "",
try dom_node.Node.toInterface(node._node),
);
defer remote_object.deinit();
const arena = cmd.arena;
return cmd.sendResult(.{ .object = .{
.type = try remote_object.getType(arena),
.subtype = try remote_object.getSubtype(arena),
.className = try remote_object.getClassName(arena),
.description = try remote_object.getDescription(arena),
.objectId = try remote_object.getObjectId(arena),
} }, .{});
// const params = (try cmd.params(struct {
// nodeId: ?Node.Id = null,
// backendNodeId: ?Node.Id = null,
// objectId: ?[]const u8 = null,
// depth: u32 = 1,
// pierce: bool = false,
// })) orelse return error.InvalidParams;
// if (params.backendNodeId != null or params.depth != 1 or params.pierce) {
// return error.NotYetImplementedParams;
// }
// const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
// if (params.nodeId != null) {
// const node = bc.node_registry.lookup_by_id.get(params.nodeId.?) orelse return error.NodeNotFound;
// return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{}) }, .{});
// }
// if (params.objectId != null) {
// // Retrieve the object from which ever context it is in.
// const parser_node = try bc.session.inspector.getNodePtr(cmd.arena, params.objectId.?);
// const node = try bc.node_registry.register(@ptrCast(parser_node));
// return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{}) }, .{});
// }
// return error.MissingParams;
}
const testing = @import("../testing.zig");

View File

@@ -112,15 +112,15 @@ fn createIsolatedWorld(cmd: anytype) !void {
}
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
try bc.createIsolatedWorld(params.worldName, params.grantUniveralAccess);
const world = &bc.isolated_world.?;
world.name = try bc.arena.dupe(u8, params.worldName);
world.grant_universal_access = params.grantUniveralAccess;
// Create the auxdata json for the contextCreated event
// Calling contextCreated will assign a Id to the context and send the contextCreated event
const aux_data = try std.fmt.allocPrint(cmd.arena, "{{\"isDefault\":false,\"type\":\"isolated\",\"frameId\":\"{s}\"}}", .{params.frameId});
bc.session.inspector.contextCreated(world.executor, world.name, "", aux_data, false);
bc.inspector.contextCreated(world.scope, world.name, "", aux_data, false);
return cmd.sendResult(.{ .executionContextId = world.executor.context.debugContextId() }, .{});
return cmd.sendResult(.{ .executionContextId = world.scope.context.debugContextId() }, .{});
}
fn navigate(cmd: anytype) !void {
@@ -222,8 +222,8 @@ pub fn pageNavigate(bc: anytype, event: *const Notification.PageNavigate) !void
const aux_json = try std.fmt.bufPrint(&buffer, "{{\"isDefault\":false,\"type\":\"isolated\",\"frameId\":\"{s}\"}}", .{bc.target_id.?});
// Calling contextCreated will assign a new Id to the context and send the contextCreated event
bc.session.inspector.contextCreated(
isolated_world.executor,
bc.inspector.contextCreated(
isolated_world.scope,
isolated_world.name,
"://",
aux_json,

View File

@@ -44,10 +44,7 @@ fn sendInspector(cmd: anytype, action: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
// the result to return is handled directly by the inspector.
bc.session.callInspector(cmd.input.json);
// force running micro tasks after send input to the inspector.
cmd.cdp.browser.runMicrotasks();
bc.callInspector(cmd.input.json);
}
fn logInspector(cmd: anytype, action: anytype) !void {

View File

@@ -122,14 +122,9 @@ fn createTarget(cmd: anytype) !void {
const target_id = cmd.cdp.target_id_gen.next();
// start the js env
const aux_data = try std.fmt.allocPrint(
cmd.arena,
// NOTE: we assume this is the default web page
"{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}",
.{target_id},
);
_ = try bc.session.createPage(aux_data);
try bc.createIsolatedWorld();
_ = try bc.session.createPage();
// change CDP state
bc.security_origin = "://";
@@ -219,6 +214,10 @@ fn closeTarget(cmd: anytype) !void {
}
bc.session.removePage();
if (bc.isolated_world) |*world| {
world.deinit();
bc.isolated_world = null;
}
bc.target_id = null;
}
@@ -520,10 +519,6 @@ test "cdp.target: createTarget" {
{
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .browserContextId = "BID-9" } });
try testing.expectEqual(true, bc.target_id != null);
try testing.expectEqual(
\\{"isDefault":true,"type":"default","frameId":"TID-1"}
, bc.session.aux_data);
try ctx.expectSentResult(.{ .targetId = bc.target_id.? }, .{ .id = 10 });
try ctx.expectSentEvent("Target.targetCreated", .{ .targetInfo = .{ .url = "about:blank", .title = "about:blank", .attached = false, .type = "page", .canAccessOpener = false, .browserContextId = "BID-9", .targetId = bc.target_id.? } }, .{});
}
@@ -545,7 +540,7 @@ test "cdp.target: closeTarget" {
}
// pretend we createdTarget first
_ = try bc.session.createPage(null);
_ = try bc.session.createPage();
bc.target_id = "TID-A";
{
try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "TID-8" } }));
@@ -576,7 +571,7 @@ test "cdp.target: attachToTarget" {
}
// pretend we createdTarget first
_ = try bc.session.createPage(null);
_ = try bc.session.createPage();
bc.target_id = "TID-B";
{
try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-8" } }));
@@ -620,7 +615,7 @@ test "cdp.target: getTargetInfo" {
}
// pretend we createdTarget first
_ = try bc.session.createPage(null);
_ = try bc.session.createPage();
bc.target_id = "TID-A";
{
try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.getTargetInfo", .params = .{ .targetId = "TID-8" } }));