mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-30 07:31:47 +00:00
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:
@@ -25,6 +25,7 @@ const Env = @import("../browser/env.zig").Env;
|
||||
const asUint = @import("../str/parser.zig").asUint;
|
||||
const Browser = @import("../browser/browser.zig").Browser;
|
||||
const Session = @import("../browser/browser.zig").Session;
|
||||
const Inspector = @import("../browser/env.zig").Env.Inspector;
|
||||
const Incrementing = @import("../id.zig").Incrementing;
|
||||
const Notification = @import("../notification.zig").Notification;
|
||||
|
||||
@@ -309,40 +310,51 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
node_registry: Node.Registry,
|
||||
node_search_list: Node.Search.List,
|
||||
|
||||
isolated_world: ?IsolatedWorld(Env),
|
||||
inspector: Inspector,
|
||||
isolated_world: ?IsolatedWorld,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
fn init(self: *Self, id: []const u8, cdp: *CDP_T) !void {
|
||||
const allocator = cdp.allocator;
|
||||
|
||||
const session = try cdp.browser.newSession(self);
|
||||
const arena = session.arena.allocator();
|
||||
|
||||
const inspector = try cdp.browser.env.newInspector(arena, self);
|
||||
|
||||
var registry = Node.Registry.init(allocator);
|
||||
errdefer registry.deinit();
|
||||
|
||||
const session = try cdp.browser.newSession(self);
|
||||
self.* = .{
|
||||
.id = id,
|
||||
.cdp = cdp,
|
||||
.arena = arena,
|
||||
.target_id = null,
|
||||
.session_id = null,
|
||||
.session = session,
|
||||
.security_origin = URL_BASE,
|
||||
.secure_context_type = "Secure", // TODO = enum
|
||||
.loader_id = LOADER_ID,
|
||||
.session = session,
|
||||
.arena = session.arena.allocator(),
|
||||
.page_life_cycle_events = false, // TODO; Target based value
|
||||
.node_registry = registry,
|
||||
.node_search_list = undefined,
|
||||
.isolated_world = null,
|
||||
.inspector = inspector,
|
||||
};
|
||||
self.node_search_list = Node.Search.List.init(allocator, &self.node_registry);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
if (self.isolated_world) |isolated_world| {
|
||||
isolated_world.executor.endScope();
|
||||
self.cdp.browser.env.stopExecutor(isolated_world.executor);
|
||||
self.isolated_world = null;
|
||||
self.inspector.deinit();
|
||||
|
||||
// If the session has a page, we need to clear it first. The page
|
||||
// context is always nested inside of the isolated world context,
|
||||
// so we need to shutdown the page one first.
|
||||
self.cdp.browser.closeSession();
|
||||
|
||||
if (self.isolated_world) |*world| {
|
||||
world.deinit();
|
||||
}
|
||||
self.node_registry.deinit();
|
||||
self.node_search_list.deinit();
|
||||
@@ -353,25 +365,25 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
self.node_search_list.reset();
|
||||
}
|
||||
|
||||
pub fn createIsolatedWorld(
|
||||
self: *Self,
|
||||
world_name: []const u8,
|
||||
grant_universal_access: bool,
|
||||
) !void {
|
||||
if (self.isolated_world != null) return error.CurrentlyOnly1IsolatedWorldSupported;
|
||||
pub fn createIsolatedWorld(self: *Self) !void {
|
||||
if (self.isolated_world != null) {
|
||||
return error.CurrentlyOnly1IsolatedWorldSupported;
|
||||
}
|
||||
|
||||
const executor = try self.cdp.browser.env.startExecutor(@import("../browser/html/window.zig").Window, &self.session.state, self.session, .isolated);
|
||||
errdefer self.cdp.browser.env.stopExecutor(executor);
|
||||
|
||||
// TBD should we endScope on removePage and re-startScope on createPage?
|
||||
// Window will be refactored into the executor so we leave it ugly here for now as a reminder.
|
||||
try executor.startScope(@import("../browser/html/window.zig").Window{});
|
||||
var executor = try self.cdp.browser.env.newExecutor();
|
||||
errdefer executor.deinit();
|
||||
|
||||
self.isolated_world = .{
|
||||
.name = try self.arena.dupe(u8, world_name),
|
||||
.grant_universal_access = grant_universal_access,
|
||||
.name = "",
|
||||
.global = .{},
|
||||
.scope = undefined,
|
||||
.executor = executor,
|
||||
.grant_universal_access = false,
|
||||
};
|
||||
var world = &self.isolated_world.?;
|
||||
|
||||
// TODO: can we do something better than passing `undefined` for the state?
|
||||
world.scope = try world.executor.startScope(&world.global, undefined, {});
|
||||
}
|
||||
|
||||
pub fn nodeWriter(self: *Self, node: *const Node, opts: Node.Writer.Opts) Node.Writer {
|
||||
@@ -384,18 +396,35 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
|
||||
pub fn getURL(self: *const Self) ?[]const u8 {
|
||||
const page = self.session.currentPage() orelse return null;
|
||||
return if (page.url) |*url| url.raw else null;
|
||||
const raw_url = page.url.raw;
|
||||
return if (raw_url.len == 0) null else raw_url;
|
||||
}
|
||||
|
||||
pub fn notify(ctx: *anyopaque, notification: *const Notification) !void {
|
||||
const self: *Self = @alignCast(@ptrCast(ctx));
|
||||
|
||||
switch (notification.*) {
|
||||
.context_created => |cc| {
|
||||
const aux_data = try std.fmt.allocPrint(self.arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{self.target_id.?});
|
||||
self.inspector.contextCreated(
|
||||
self.session.page.?.scope,
|
||||
"",
|
||||
cc.origin,
|
||||
aux_data,
|
||||
true,
|
||||
);
|
||||
},
|
||||
.page_navigate => |*pn| return @import("domains/page.zig").pageNavigate(self, pn),
|
||||
.page_navigated => |*pn| return @import("domains/page.zig").pageNavigated(self, pn),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn callInspector(self: *const Self, msg: []const u8) void {
|
||||
self.inspector.send(msg);
|
||||
// force running micro tasks after send input to the inspector.
|
||||
self.cdp.browser.runMicrotasks();
|
||||
}
|
||||
|
||||
pub fn onInspectorResponse(ctx: *anyopaque, _: u32, msg: []const u8) void {
|
||||
if (std.log.defaultLogEnabled(.debug)) {
|
||||
// msg should be {"id":<id>,...
|
||||
@@ -481,13 +510,17 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
/// An isolated world has it's own instance of globals like Window.
|
||||
/// Generally the client needs to resolve a node into the isolated world to be able to work with it.
|
||||
/// An object id is unique across all contexts, different object ids can refer to the same Node in different contexts.
|
||||
pub fn IsolatedWorld(comptime E: type) type {
|
||||
return struct {
|
||||
name: []const u8,
|
||||
grant_universal_access: bool,
|
||||
executor: *E.Executor,
|
||||
};
|
||||
}
|
||||
const IsolatedWorld = struct {
|
||||
name: []const u8,
|
||||
scope: *Env.Scope,
|
||||
executor: Env.Executor,
|
||||
grant_universal_access: bool,
|
||||
global: @import("../browser/html/window.zig").Window,
|
||||
|
||||
pub fn deinit(self: *IsolatedWorld) void {
|
||||
self.executor.deinit();
|
||||
}
|
||||
};
|
||||
|
||||
// This is a generic because when we send a result we have two different
|
||||
// behaviors. Normally, we're sending the result to the client. But in some cases
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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" } }));
|
||||
|
||||
@@ -122,7 +122,7 @@ const TestContext = struct {
|
||||
if (opts.html) |html| {
|
||||
parser.deinit();
|
||||
try parser.init();
|
||||
const page = try bc.session.createPage(null);
|
||||
const page = try bc.session.createPage();
|
||||
page.doc = (try Document.init(html)).doc;
|
||||
}
|
||||
return bc;
|
||||
|
||||
Reference in New Issue
Block a user